Compare commits
102 Commits
9d0ec1f22e
...
main
@@ -15,6 +15,11 @@ ZAI_API_KEY=
|
||||
ZAI_GENERAL_ENDPOINT=https://api.z.ai/api/paas/v4
|
||||
ZAI_CODING_ENDPOINT=https://api.z.ai/api/coding/paas/v4
|
||||
|
||||
# OpenRouter API
|
||||
# Get API key from https://openrouter.ai/keys
|
||||
OPENROUTER_API_KEY=
|
||||
OPENROUTER_DEFAULT_MODEL=google/gemini-2.0-flash-exp:free
|
||||
|
||||
# Site Configuration (Required for OAuth in production)
|
||||
# Set to your production URL (e.g., https://your-app.vercel.app)
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:6002
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,3 +37,4 @@ next-env.d.ts
|
||||
# logs
|
||||
logs
|
||||
*.log
|
||||
fix_*.py
|
||||
|
||||
324
CHANGELOG.md
Normal file
324
CHANGELOG.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# Changelog
|
||||
|
||||
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/),
|
||||
and this project adheres to [Semantic Versioning]
|
||||
## [2.2.1] - 2026-03-19 05:24 UTC
|
||||
|
||||
### Added
|
||||
- **Leads Canvas Preview** - Leads Finder now renders results as an Excel-like HTML table in the canvas, with columns: #, Name, Platform, Followers, Region, Bio, Link
|
||||
- **Dark-Themed Leads Report** - Beautiful dark UI with emerald accents, stats grid (total leads, combined reach, top platform, top region), platform-colored badges (Instagram, Twitter, LinkedIn, YouTube, TikTok), and hover effects
|
||||
- **Auto Canvas** - Leads Finder is now a visual agent that auto-opens the canvas when preview data is available
|
||||
|
||||
### Fixed
|
||||
- **Build Error** - Restored `<Badge variant="outline">` for plan card tech stack divs that were incorrectly replaced with plain `<div>` by an over-broad fix script
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 2 (AIAssist.tsx, openrouter.ts)
|
||||
|
||||
## [2.2.0] - 2026-03-19 04:44 UTC
|
||||
|
||||
### Added
|
||||
- **Leads Finder Agent** - New agent mode in Vibe Architect that finds relevant influencers, prospects, and leads across social media platforms (Instagram, Twitter/X, LinkedIn, YouTube, TikTok)
|
||||
- **Auto Web Search** - Leads Finder automatically enables web search to find real-time leads data
|
||||
- **Structured Lead Format** - Returns leads in consistent format: Name | Followers | Region | Location with description and social URL
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 3 (AIAssist.tsx, translations.ts, openrouter.ts)
|
||||
|
||||
|
||||
## [2.1.0] - 2026-03-18 22:06 UTC
|
||||
|
||||
### Added
|
||||
- **Comprehensive 17-Section Report** — Full rebuild of SEO/GEO audit report generator with all industry-standard sections:
|
||||
- Executive Summary with stats grid
|
||||
- 6-Category Scoring Breakdown with progress bars (Overall, Technical, Content, Performance, Social, GEO)
|
||||
- Meta Tags Analysis (9 checks: title, description, keywords, viewport, charset, canonical, robots, X-Frame-Options, protocol)
|
||||
- Social & Open Graph (8 properties with pass/fail status)
|
||||
- Heading Structure with hierarchy visualization
|
||||
- Links Analysis with sample external links table
|
||||
- Images & Alt Text with missing-alt report
|
||||
- Content Analysis with readability recommendations
|
||||
- Performance Signals (12 metrics including encoding, scripts, preconnect, preload, DNS prefetch, async/defer)
|
||||
- Accessibility (4 checks: lang, ARIA, first-image alt, alt coverage)
|
||||
- Structured Data / Schema detection
|
||||
- Hreflang Tags
|
||||
- GEO Analysis with readiness score, 4 sub-scores (Factual, Entity, Content Depth, Citeability), and improvement checklist
|
||||
- Issues & How to Fix (severity badges, category-specific fix instructions)
|
||||
- Prioritized Action Plan (high/medium/low with impact labels)
|
||||
- Recommended FAQ Schema Questions (auto-generated based on domain)
|
||||
- SEO/GEO Best Practices Checklist (12 items)
|
||||
- **GEO Scoring Engine** — 0-100 GEO readiness score based on schema markup, content depth, heading hierarchy, and citeability signals
|
||||
- **Auto-Generated Action Plan** — 20+ prioritized fixes derived from audit data with severity and impact labels
|
||||
- **Print-Optimized CSS** — Clean white-on-dark print styles for professional PDF output
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 1 (seo-report.ts) — complete rewrite, 530+ lines
|
||||
|
||||
|
||||
## [2.0.1] - 2026-03-18 21:56 UTC
|
||||
|
||||
### Fixed
|
||||
- **PDF Export** — Replaced `window.open` with hidden iframe approach for reliable print dialog trigger (no popup blocker issues)
|
||||
- **PDF Export** — Initial fix using Blob URL (superseded by iframe approach)
|
||||
|
||||
### Added
|
||||
- **Inline SEO Export Buttons** — Export HTML/PDF buttons now appear directly in chat messages after SEO audit, not just in post-coding action bar
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 2 (AIAssist.tsx, seo-report.ts)
|
||||
|
||||
|
||||
## [2.0.0] - 2026-03-18 21:33 UTC
|
||||
|
||||
### Added
|
||||
- **SEO Audit Export** — Export SEO/GEO audit reports as HTML or PDF with comprehensive fix instructions
|
||||
- **SEO Report Generator** — Standalone `lib/seo-report.ts` utility with color-coded scores, issue tables with fix instructions, and print-friendly CSS
|
||||
- **Default Vibe Architect** — Welcome screen now opens to Vibe Architect by default (was Prompt Enhancer)
|
||||
- **Vibe Architect Dedicated Route** — Full-screen immersive mode at `/vibe` with `vibeMode` prop
|
||||
|
||||
### Changed
|
||||
- **General Agent Plain Chat** — General mode no longer triggers plan/code flow, now works as plain chat
|
||||
- **SEO Follow-up Fix** — Non-visual agents (SEO, content, SMM) preserve existing canvas on follow-up messages
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 3 (AIAssist.tsx, page.tsx)
|
||||
- Files added: 2 (seo-report.ts, vibe/page.tsx)
|
||||
|
||||
(https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.9.0] - 2026-03-18 21:05 UTC
|
||||
|
||||
### Changed
|
||||
- **Global Rebrand: AI Assist -> Vibe Architect** — All instances renamed across UI, sidebar, translations (EN/RU/HE)
|
||||
- **Sidebar Highlight** — Vibe Architect button now has a gradient blue-purple border and bold blue text when not active, making it stand out in the navigation menu
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 3 (Sidebar.tsx, AIAssist.tsx, translations.ts)
|
||||
|
||||
## [1.8.0] - 2026-03-18 21:02 UTC
|
||||
|
||||
### Added
|
||||
- **Vibe Architect Dedicated Mode** — Full-screen immersive AI coding experience
|
||||
- New route: `/vibe` ([rommark.dev/tools/promptarch/vibe/](https://rommark.dev/tools/promptarch/vibe/))
|
||||
- No sidebar, no navigation — just AI Assist in full screen
|
||||
- Rebranded as "Vibe Architect" with dedicated messaging
|
||||
- `vibeMode` prop on AIAssist component for label overrides
|
||||
|
||||
### Fixed
|
||||
- **SEO follow-up preview** — Canvas no longer breaks when asking follow-up questions after SEO audit
|
||||
- `isModifying` overlay only triggers for visual agents (code, web, app, design) — not SEO/content/SMM
|
||||
- Non-visual agent follow-ups preserve existing canvas if no new preview is generated
|
||||
|
||||
### Technical Details
|
||||
- Files added: 1 (`app/vibe/page.tsx`)
|
||||
- Files modified: 1 (AIAssist.tsx: +8/-3 lines)
|
||||
- New prop: `vibeMode` on AIAssist component
|
||||
|
||||
## [1.7.0] - 2026-03-18 20:44 UTC
|
||||
|
||||
### Added
|
||||
- **Comprehensive SEO Audit Engine** — Industry-grade URL analysis modeled after Screaming Frog, Ahrefs, Sitebulb, and Semrush
|
||||
- Full meta tag analysis: title (length/status), description (length/status), keywords, viewport, charset, robots directives
|
||||
- Open Graph & Twitter Card validation
|
||||
- Heading structure audit: H1 count check, hierarchy analysis (H1-H6), multiple H1 detection
|
||||
- Link analysis: internal/external/nofollow counts, external link sampling
|
||||
- Image audit: total count, alt text coverage %, lazy loading detection, missing alt samples
|
||||
- Content analysis: word count, sentence count, paragraph count, avg words/sentence
|
||||
- Structured data detection: JSON-LD, Microdata, 12+ schema types (Article, FAQ, Product, LocalBusiness, etc.)
|
||||
- Performance signals: server info, response time, HTML size, external scripts/stylesheets, inline styles, preconnect/preload/dns-prefetch, async/defer detection
|
||||
- Accessibility checks: lang attribute, ARIA labels, first image alt text
|
||||
- Hreflang tag detection for international SEO
|
||||
- Canonical tag validation with mismatch detection
|
||||
- **Automated scoring**: Overall, Technical, Content, Performance, Social scores (0-100)
|
||||
- **Issue detection**: Critical/Warning/Info severity levels across all categories
|
||||
- **Plan-First Flow Fix** — Non-code agents (SEO, content, SMM, design, web, app) now bypass the plan-first workflow
|
||||
- No more "Start Coding" or "Modify Plan" buttons for non-code agent responses
|
||||
- SEO agent goes straight to preview mode after generating audit reports
|
||||
- Plan card only shown for code/general agents
|
||||
|
||||
### Changed
|
||||
- `/api/fetch-url` route completely rewritten: 300+ lines of comprehensive HTML parsing and scoring
|
||||
- SEO agent data injection now passes structured audit data with scores, issues, and category breakdowns
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 5 (AIAssist.tsx, qwen-oauth.ts, ollama-cloud.ts, zai-plan.ts, openrouter.ts)
|
||||
- Files rewritten: 1 (`app/api/fetch-url/route.ts` — complete rewrite)
|
||||
- New component-level guard: `isCodeAgent` condition for plan-first workflow
|
||||
|
||||
## [1.6.0] - 2026-03-18 20:34 UTC
|
||||
|
||||
### Added
|
||||
- **SEO Web Audit** — SEO agent can now fetch and analyze live websites
|
||||
- `/api/fetch-url` route extracts: title, meta tags, headings (h1-h6), links (internal/external), images (alt text coverage), canonical URL, OG tags, text content length
|
||||
- Auto-detects URLs in user input when SEO agent is active
|
||||
- Pre-fetches page data before sending to AI for comprehensive audit reports
|
||||
- Supports up to 2 URLs per request
|
||||
- **SEO Auto Web Search** — When SEO agent is active and no URL is provided, web search is automatically triggered
|
||||
- Uses same SearXNG infrastructure as manual web search toggle
|
||||
- No manual toggle needed in SEO mode
|
||||
- **Updated SEO Agent Prompt** — All 4 AI services now instruct SEO agent about web audit and search capabilities
|
||||
- Added `[WEB_AUDIT:url]` and `[WEB_SEARCH:query]` tool markers
|
||||
- Added "WEB TOOLS" section to system prompts
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 5 (AIAssist.tsx, qwen-oauth.ts, ollama-cloud.ts, zai-plan.ts, openrouter.ts)
|
||||
- Files added: 1 (`app/api/fetch-url/route.ts`)
|
||||
- New API endpoint: `GET /api/fetch-url?url=https://example.com`
|
||||
|
||||
## [1.5.0] - 2026-03-18 20:29 UTC
|
||||
|
||||
### Added
|
||||
- **Modification Progress Overlay** — Visually appealing spinner when modifying existing generated code
|
||||
- Full-screen canvas overlay with spinning ring + Wrench icon
|
||||
- "Modification in Progress" label with animated bouncing dots
|
||||
- Canvas hidden during modification, revealed only when AI finishes
|
||||
- `isModifying` state tracked via `assistStep === "preview"` detection
|
||||
|
||||
### Fixed
|
||||
- **Preview blink during modification** — Old preview no longer flashes while AI rewrites code
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 1 (AIAssist.tsx: +21/-2 lines)
|
||||
- New state:
|
||||
- New import: from lucide-react
|
||||
|
||||
|
||||
## [1.4.0] - 2026-03-18 19:57 UTC
|
||||
|
||||
### 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)
|
||||
184
README.md
184
README.md
@@ -1,4 +1,6 @@
|
||||
# PromptArch: The Prompt Enhancer 🚀
|
||||
# PromptArch: AI Orchestration Platform
|
||||
|
||||
> **Latest Version**: [v2.2.1](CHANGELOG.md#2.2.1---2026-03-19) (2026-03-19)(CHANGELOG.md#2.1.0---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.1---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.0---2026-03-18) (2026-03-18)(CHANGELOG.md#190---2026-03-18) (2026-03-18)
|
||||
|
||||
> **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
|
||||
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
|
||||
@@ -7,35 +9,68 @@
|
||||
|
||||
> **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 elite AI orchestration platform designed for software architects and Vibe Coders.
|
||||
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.
|
||||
|
||||
**Developed by [Roman | RyzenAdvanced](https://github.com/roman-ryzenadvanced)**
|
||||
**Developed by Roman | RyzenAdvanced**
|
||||
|
||||
- 📦 **GitHub Repository**: [roman-ryzenadvanced/PromptArch-the-prompt-enhancer](https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer)
|
||||
- 📮 **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/)
|
||||
- **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||
|
||||
## 🌟 Visual Overview
|
||||
## Core Capabilities
|
||||
|
||||
### 🛠 Core Capabilities
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Vibe Architect** | Plan-first workflow: describe a task, get a structured plan, approve, then generate working code with live preview |
|
||||
| **Prompt Enhancer** | Refine vague prompts into surgical instructions using 9 enhancement strategies and 11+ intent patterns |
|
||||
| **PRD Generator** | Convert ideas into structured Product Requirements Documents |
|
||||
| **Action Plan** | Decompose PRDs into actionable development steps and framework recommendations |
|
||||
| **Google Ads Generator** | Generate ad campaigns with XLSX and HTML report export |
|
||||
| **Slides Generator** | Create presentation decks from prompts |
|
||||
| **Market Researcher** | AI-powered market research and analysis |
|
||||
|
||||
- **Prompt Enhancer**: Refine vague prompts into surgical instructions for AI agents.
|
||||
- **PRD Generator**: Convert ideas into structured Product Requirements Documents.
|
||||
- **Action Plan**: Decompose PRDs into actionable development steps and framework recommendations.
|
||||
## Features
|
||||
|
||||
## ✨ Features
|
||||
### Plan-First Workflow (v1.3.0)
|
||||
- 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
|
||||
- After code generation: **Preview** canvas + **Request Modifications** buttons
|
||||
- Streaming plan mode with real-time parsing and canvas suppression
|
||||
|
||||
- **Multi-Provider Ecosystem**: Native support for Qwen Code (OAuth), Ollama Cloud, and Z.AI Plan API.
|
||||
- **Visual Prompt Engineering**: Patterns-based enhancement with 11+ intent types.
|
||||
- **Architectural Decomposition**: Automatic generation of PRDs and structured Action Plans.
|
||||
- **Resilient Fallbacks**: Multi-tier provider system that ensures uptime even if primary APIs fail.
|
||||
- **Modern UI/UX**: Built with Next.js 15, Tailwind CSS, and shadcn/ui for a seamless developer experience.
|
||||
- **OAuth Integration**: Secure Qwen authentication with 2,000 free daily requests.
|
||||
### Multi-Provider AI (4 Providers)
|
||||
| Provider | Auth | Models |
|
||||
|----------|------|--------|
|
||||
| **Qwen Code** | OAuth (2,000 free req/day) | Qwen Coder models |
|
||||
| **Ollama Cloud** | API Key | Open-source models |
|
||||
| **Z.AI Plan** | API Key | GLM general + coding models |
|
||||
| **OpenRouter** | API Key | 20+ models (Gemini, Llama, Mistral, etc.) |
|
||||
|
||||
## 🚀 Quick Start
|
||||
### Visual Canvas
|
||||
- Live code rendering with `[PREVIEW]` tags
|
||||
- HTML, React, Python, and more — rendered in-browser
|
||||
- Auto-detect renderable vs. code-only previews
|
||||
- Responsive preview with device size selector (Full / Desktop / Tablet / Mobile)
|
||||
|
||||
### Code Review & Web Search
|
||||
- **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
|
||||
|
||||
### Enhanced Prompt Engine
|
||||
- 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
|
||||
- 11+ intent detection patterns (coding, creative, analysis, etc.)
|
||||
|
||||
### Other
|
||||
- Multi-language support (English, Russian, Hebrew)
|
||||
- Download generated artifacts as ZIP
|
||||
- Push to GitHub integration
|
||||
- Resilient multi-tier provider fallbacks
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Clone & Install**:
|
||||
```bash
|
||||
git clone https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git
|
||||
git clone https://github.rommark.dev/admin/PromptArch.git
|
||||
cd PromptArch
|
||||
npm install
|
||||
```
|
||||
@@ -46,6 +81,12 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an e
|
||||
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)
|
||||
- **Z.AI**: Get API key from [docs.z.ai](https://docs.z.ai)
|
||||
- **OpenRouter**: Get API key from [openrouter.ai/keys](https://openrouter.ai/keys) (free tier available)
|
||||
|
||||
3. **Launch**:
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -53,48 +94,91 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an e
|
||||
|
||||
4. Open [http://localhost:3000](http://localhost:3000) to begin.
|
||||
|
||||
## 🛠 Tech Stack
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: [Next.js 15.5](https://nextjs.org/) (App Router)
|
||||
- **Styling**: [Tailwind CSS](https://tailwindcss.com/)
|
||||
- **State Management**: [Zustand](https://zustand-demo.pmnd.rs/)
|
||||
- **Components**: [shadcn/ui](https://ui.shadcn.com/)
|
||||
- **Icons**: [Lucide React](https://lucide.dev/)
|
||||
- **Framework**: Next.js 15 (App Router, Turbopack)
|
||||
- **Styling**: Tailwind CSS
|
||||
- **State Management**: Zustand
|
||||
- **Components**: shadcn/ui (Radix UI)
|
||||
- **Icons**: Lucide React
|
||||
- **Markdown**: react-markdown
|
||||
- **Language**: TypeScript
|
||||
|
||||
## 🤝 Attribution & Credits
|
||||
## Project Structure
|
||||
|
||||
**Author**: [Roman | RyzenAdvanced](https://github.com/roman-ryzenadvanced)
|
||||
- 📦 **GitHub**: [roman-ryzenadvanced/PromptArch-the-prompt-enhancer](https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer)
|
||||
- 📮 **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||
```
|
||||
promptarch/
|
||||
components/
|
||||
AIAssist.tsx # Main AI chat with plan-first workflow (1453 lines)
|
||||
PromptEnhancer.tsx # Prompt enhancement UI with intent detection (556 lines)
|
||||
SettingsPanel.tsx # Provider configuration and API key management (569 lines)
|
||||
Sidebar.tsx # Navigation sidebar
|
||||
GoogleAdsGenerator.tsx # Google Ads campaign generator
|
||||
PRDGenerator.tsx # Product Requirements Document generator
|
||||
ActionPlanGenerator.tsx # Action plan decomposition
|
||||
SlidesGenerator.tsx # Presentation deck generator
|
||||
MarketResearcher.tsx # Market research tool
|
||||
HistoryPanel.tsx # Chat history management
|
||||
lib/
|
||||
enhance-engine.ts # Modular prompt enhancement (9 strategies)
|
||||
store.ts # Zustand state store
|
||||
artifact-utils.ts # Preview/rendering utilities
|
||||
export-utils.ts # Export to XLSX/HTML/ZIP
|
||||
services/
|
||||
qwen-oauth.ts # Qwen OAuth streaming service
|
||||
ollama-cloud.ts # Ollama Cloud streaming service
|
||||
zai-plan.ts # Z.AI Plan streaming service
|
||||
openrouter.ts # OpenRouter streaming service
|
||||
model-adapter.ts # Unified provider adapter
|
||||
adapter-instance.ts # Provider registry
|
||||
i18n/
|
||||
translations.ts # EN/RU/HE translations
|
||||
types/
|
||||
index.ts # TypeScript interfaces
|
||||
```
|
||||
|
||||
**Forked from**: [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix)
|
||||
- This project is a visual and architectural evolution of the Clavix framework
|
||||
- Clavix focuses on agentic-first Markdown templates
|
||||
- PromptArch provides a centralized web interface with advanced model orchestration
|
||||
## Versioning
|
||||
|
||||
**Development Platform**: [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW)
|
||||
- 100% AI-assisted development using TRAE.AI's advanced coding capabilities
|
||||
- Learn more about the architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW)
|
||||
This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). See [CHANGELOG.md](CHANGELOG.md) for detailed release notes.
|
||||
|
||||
| Version | Date | Highlights |
|
||||
|---------|------|------------|
|
||||
| [2.2.0](CHANGELOG.md#2.2.0---2026-03-19) | Leads Finder Agent Mode | 2026-03-19 |
|
||||
| [2.1.0](CHANGELOG.md#2.1.0---2026-03-18) | Full 17-Section Report, GEO Scoring, Action Plan, FAQ Gen | 2026-03-18 |
|
||||
| [2.0.1](CHANGELOG.md#2.0.1---2026-03-18) | Inline SEO Export, PDF Print Fix | 2026-03-18 |
|
||||
| [2.0.0](CHANGELOG.md#2.0.0---2026-03-18) | SEO Export, Default Vibe, /vibe Route, General Chat | 2026-03-18 |
|
||||
| [1.9.0](CHANGELOG.md#190---2026-03-18) | 2026-03-18 21:05 UTC | Vibe Architect rebrand, sidebar highlight |
|
||||
| [1.8.0](CHANGELOG.md#180---2026-03-18) | 2026-03-18 21:02 UTC | Vibe Architect dedicated mode, SEO follow-up fix |
|
||||
| [1.7.0](CHANGELOG.md#170---2026-03-18) | 2026-03-18 20:44 UTC | Industry-grade SEO audit, plan flow fix for non-code agents |
|
||||
| [1.6.0](CHANGELOG.md#160---2026-03-18) | 2026-03-18 20:34 | SEO web audit, URL fetching, auto web search for SEO mode |
|
||||
| [1.5.0](CHANGELOG.md#150---2026-03-18) | 2026-03-18 20:29 | Modification progress overlay, preview blink fix |
|
||||
| [1.4.0](CHANGELOG.md#140---2026-03-18) | 2026-03-18 19:57 | Review Code button, web search grounding, responsive preview, model selector fix |
|
||||
| [1.3.0](CHANGELOG.md#130---2026-03-18) | 2026-03-18 18:51 | Plan-first workflow, OpenRouter, post-coding UX, enhanced prompt engine |
|
||||
| [1.2.0](CHANGELOG.md#120---2026-01-19) | 2026-01-19 19:16 | SEO agent fixes, Z.AI API validation |
|
||||
| [1.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 |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run development server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Start production server
|
||||
npm start
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
npm install # Install dependencies
|
||||
npm run dev # Development server (Turbopack)
|
||||
npm run build # Production build
|
||||
npm start # Start production server
|
||||
npm run lint # Lint code
|
||||
```
|
||||
|
||||
## Attribution & Credits
|
||||
|
||||
**Author**: Roman | RyzenAdvanced
|
||||
- **Gitea**: [admin/PromptArch](https://github.rommark.dev/admin/PromptArch)
|
||||
- **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||
|
||||
**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
|
||||
|
||||
108
app/api/ai-assist/route.ts
Normal file
108
app/api/ai-assist/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { randomUUID } from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
// Schema validation
|
||||
const schema = z.object({
|
||||
request: z.string().min(1),
|
||||
step: z.enum(["plan", "generate", "preview"]).default("plan"),
|
||||
plan: z.any().optional(),
|
||||
code: z.string().optional(),
|
||||
provider: z.string().optional(),
|
||||
model: z.string().optional()
|
||||
});
|
||||
|
||||
const STEPS = {
|
||||
plan: `You are an expert software architect. Create a DETAILED DEVELOPMENT PLAN for the following request: "{request}"
|
||||
|
||||
Output ONLY a JSON object:
|
||||
{
|
||||
"summary": "One sentence overview",
|
||||
"architecture": "High-level components + data flow",
|
||||
"techStack": ["Next.js", "Tailwind", "Lucide Icons"],
|
||||
"files": [
|
||||
{"path": "app/page.tsx", "purpose": "Main UI"},
|
||||
{"path": "components/Preview.tsx", "purpose": "Core logic"}
|
||||
],
|
||||
"timeline": "Estimate",
|
||||
"risks": ["Potential blockers"]
|
||||
}`,
|
||||
|
||||
generate: `You are a Senior Vibe Coder. Execute the following approved plan:
|
||||
Plan: {plan}
|
||||
|
||||
Generate COMPLETE, PRODUCTION-READY code for all files.
|
||||
Focus on the request: "{request}"
|
||||
|
||||
Output ONLY a JSON object:
|
||||
{
|
||||
"files": {
|
||||
"app/page.tsx": "// code here",
|
||||
"components/UI.tsx": "// more code"
|
||||
},
|
||||
"explanation": "How it works"
|
||||
}`,
|
||||
|
||||
preview: `Convert the following code into a single-file interactive HTML preview (Standalone).
|
||||
Use Tailwind CDN.
|
||||
|
||||
Code: {code}
|
||||
|
||||
Output ONLY valid HTML.`
|
||||
};
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const requestId = randomUUID();
|
||||
|
||||
try {
|
||||
// Safe body parsing
|
||||
const body = await req.json().catch(() => null);
|
||||
|
||||
if (!body) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid JSON body", requestId, success: false },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate schema
|
||||
const parseResult = schema.safeParse(body);
|
||||
if (!parseResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid request body",
|
||||
details: parseResult.error.flatten(),
|
||||
requestId,
|
||||
success: false
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { request, step, plan, code } = parseResult.data;
|
||||
|
||||
let prompt = STEPS[step];
|
||||
prompt = prompt.replace("{request}", request);
|
||||
if (plan) prompt = prompt.replace("{plan}", JSON.stringify(plan));
|
||||
if (code) prompt = prompt.replace("{code}", code);
|
||||
|
||||
// Return the prompt for the frontend to use with the streaming adapter
|
||||
return NextResponse.json({
|
||||
prompt,
|
||||
step,
|
||||
requestId,
|
||||
success: true
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(`[ai-assist] requestId=${requestId}`, err);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: err?.message ?? "AI Assist failed",
|
||||
requestId,
|
||||
success: false
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
269
app/api/fetch-url/route.ts
Normal file
269
app/api/fetch-url/route.ts
Normal 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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<")
|
||||
.replace(/>/g, ">").replace(/"/g, '"').replace(/'/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 });
|
||||
}
|
||||
}
|
||||
@@ -38,15 +38,27 @@ export async function POST(request: NextRequest) {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const payload = await response.text();
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
return NextResponse.json(
|
||||
{ error: "Ollama chat request failed", details: payload },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(payload ? JSON.parse(payload) : {});
|
||||
// If stream is requested, pipe the response body
|
||||
if (body.stream) {
|
||||
return new Response(response.body, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-ndjson",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
return NextResponse.json(payload);
|
||||
} catch (error) {
|
||||
console.error("Ollama chat proxy failed", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -44,23 +44,27 @@ export async function POST(request: NextRequest) {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.text();
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
return NextResponse.json(
|
||||
{ error: payload || response.statusText || "Qwen chat failed" },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(payload);
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: payload || "Unexpected response format" },
|
||||
{ status: 502 }
|
||||
);
|
||||
// Handle streaming
|
||||
if (stream) {
|
||||
return new Response(response.body, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Qwen chat failed" },
|
||||
|
||||
27
app/api/search/route.ts
Normal file
27
app/api/search/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Next.js API route: Web search proxy.
|
||||
* Calls SearXNG public instances and returns top results.
|
||||
* Endpoint: GET /api/search?q=your+query
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { searchWeb } from "@/lib/services/search-api";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const query = request.nextUrl.searchParams.get("q");
|
||||
|
||||
if (!query || query.trim().length < 3) {
|
||||
return NextResponse.json({ results: [], error: "Query too short" });
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await searchWeb(query);
|
||||
return NextResponse.json({ results });
|
||||
} catch (error) {
|
||||
console.error("Search API error:", error);
|
||||
return NextResponse.json(
|
||||
{ results: [], error: "Search failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
37
app/api/slides/route.ts
Normal file
37
app/api/slides/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const schema = z.object({
|
||||
topic: z.string().min(3),
|
||||
slideCount: z.number().min(3).max(15).default(8),
|
||||
style: z.enum(["professional", "creative", "technical", "pitch"]).default("professional"),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { topic, slideCount, style } = schema.parse(body);
|
||||
|
||||
const systemPrompt = `You are an elite presentation designer. Create a visually stunning presentation with ${slideCount} slides about "${topic}".
|
||||
|
||||
Style: ${style}
|
||||
|
||||
Output ONLY a sequence of slides separated by "---".
|
||||
Format each slide as:
|
||||
## [Slide Title]
|
||||
- [Bullet Point 1]
|
||||
- [Bullet Point 2]
|
||||
VISUAL: [Detailed description of image/chart/icon]
|
||||
---
|
||||
`;
|
||||
|
||||
// The frontend will handle the actual generation call to keep use of the ModelAdapter,
|
||||
// this route serves as the prompt orchestrator.
|
||||
return NextResponse.json({
|
||||
prompt: systemPrompt,
|
||||
success: true
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -63,53 +63,71 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
|
||||
/* Mobile optimizations */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
/* Better touch targets */
|
||||
button, a, [role="button"] {
|
||||
button,
|
||||
a,
|
||||
[role="button"] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
|
||||
/* Prevent text selection on buttons */
|
||||
button {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
/* Safe area padding for notched devices */
|
||||
.safe-area-inset {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
|
||||
/* Scrollbar styling for mobile-like experience */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes progress-indeterminate {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-progress-indeterminate {
|
||||
animation: progress-indeterminate 1.5s infinite linear;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ export const metadata: Metadata = {
|
||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||
};
|
||||
|
||||
import LocaleProvider from "@/components/LocaleProvider";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
@@ -20,7 +22,11 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={roboto.className}>{children}</body>
|
||||
<body className={roboto.className}>
|
||||
<LocaleProvider>
|
||||
{children}
|
||||
</LocaleProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
31
app/page.tsx
31
app/page.tsx
@@ -3,16 +3,24 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import type { View } from "@/components/Sidebar";
|
||||
import PromptEnhancer from "@/components/PromptEnhancer";
|
||||
import PRDGenerator from "@/components/PRDGenerator";
|
||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
||||
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
||||
import HistoryPanel from "@/components/HistoryPanel";
|
||||
import SettingsPanel from "@/components/SettingsPanel";
|
||||
import dynamic from 'next/dynamic';
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
|
||||
// Dynamic imports to prevent hydration mismatches
|
||||
// ensuring hydration match
|
||||
const PromptEnhancer = dynamic(() => import("@/components/PromptEnhancer"), { ssr: false });
|
||||
const PRDGenerator = dynamic(() => import("@/components/PRDGenerator"), { ssr: false });
|
||||
const ActionPlanGenerator = dynamic(() => import("@/components/ActionPlanGenerator"), { ssr: false });
|
||||
const UXDesignerPrompt = dynamic(() => import("@/components/UXDesignerPrompt"), { ssr: false });
|
||||
const SlidesGenerator = dynamic(() => import("@/components/SlidesGenerator"), { ssr: false });
|
||||
const GoogleAdsGenerator = dynamic(() => import("@/components/GoogleAdsGenerator"), { ssr: false });
|
||||
const MarketResearcher = dynamic(() => import("@/components/MarketResearcher"), { ssr: false });
|
||||
const AIAssist = dynamic(() => import("@/components/AIAssist"), { ssr: false });
|
||||
const HistoryPanel = dynamic(() => import("@/components/HistoryPanel"), { ssr: false });
|
||||
const SettingsPanel = dynamic(() => import("@/components/SettingsPanel"), { ssr: false });
|
||||
|
||||
export default function Home() {
|
||||
const [currentView, setCurrentView] = useState<View>("enhance");
|
||||
const [currentView, setCurrentView] = useState<View>("ai-assist");
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[Home] Initializing Qwen OAuth service on client...");
|
||||
@@ -29,6 +37,14 @@ export default function Home() {
|
||||
return <ActionPlanGenerator />;
|
||||
case "uxdesigner":
|
||||
return <UXDesignerPrompt />;
|
||||
case "slides":
|
||||
return <SlidesGenerator />;
|
||||
case "googleads":
|
||||
return <GoogleAdsGenerator />;
|
||||
case "market-research":
|
||||
return <MarketResearcher />;
|
||||
case "ai-assist":
|
||||
return <AIAssist />;
|
||||
case "history":
|
||||
return <HistoryPanel />;
|
||||
case "settings":
|
||||
@@ -49,3 +65,4 @@ export default function Home() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
20
app/vibe/page.tsx
Normal file
20
app/vibe/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1840
components/AIAssist.tsx
Normal file
1840
components/AIAssist.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function ActionPlanGenerator() {
|
||||
const {
|
||||
language,
|
||||
currentPrompt,
|
||||
actionPlan,
|
||||
selectedProvider,
|
||||
@@ -28,6 +30,9 @@ export default function ActionPlanGenerator() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const t = translations[language].actionPlan;
|
||||
const common = translations[language].common;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
@@ -66,7 +71,7 @@ export default function ActionPlanGenerator() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter PRD or project requirements");
|
||||
setError(t.enterPrdError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,7 +79,7 @@ export default function ActionPlanGenerator() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,11 +112,11 @@ export default function ActionPlanGenerator() {
|
||||
setActionPlan(newPlan);
|
||||
} else {
|
||||
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate action plan");
|
||||
setError(result.error || t.errorGenerate);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[ActionPlanGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -126,20 +131,20 @@ export default function ActionPlanGenerator() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Action Plan Generator
|
||||
{t.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Convert PRD into actionable implementation plan
|
||||
{t.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
@@ -155,8 +160,8 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
@@ -171,12 +176,12 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">PRD / Requirements</label>
|
||||
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||
<Textarea
|
||||
placeholder="Paste your PRD or project requirements here..."
|
||||
placeholder={t.placeholder}
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -186,7 +191,7 @@ export default function ActionPlanGenerator() {
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -196,24 +201,24 @@ export default function ActionPlanGenerator() {
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Generating...
|
||||
{common.generating}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate Action Plan
|
||||
{t.generateButton}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!actionPlan && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Action Plan
|
||||
{t.generatedTitle}
|
||||
</span>
|
||||
{actionPlan && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
@@ -226,36 +231,35 @@ export default function ActionPlanGenerator() {
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Task breakdown, frameworks, and architecture recommendations
|
||||
{t.generatedDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{actionPlan ? (
|
||||
<div className="space-y-3 lg:space-y-4">
|
||||
<div className="rounded-md border bg-primary/5 p-3 lg:p-4">
|
||||
<div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
|
||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Implementation Roadmap
|
||||
{t.roadmap}
|
||||
</h4>
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{actionPlan.rawContent}</pre>
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Quick Notes
|
||||
{t.quickNotes}
|
||||
</h4>
|
||||
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
||||
<li>Review all task dependencies before starting</li>
|
||||
<li>Set up recommended framework architecture</li>
|
||||
<li>Follow best practices for security and performance</li>
|
||||
<li>Use specified deployment strategy</li>
|
||||
{t.notes.map((note: string, i: number) => (
|
||||
<li key={i}>{note}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||
Action plan will appear here
|
||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||
{t.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
57
components/ErrorBoundary.tsx
Normal file
57
components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AlertTriangle, RotateCcw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||
}
|
||||
|
||||
resetError = () => {
|
||||
this.setState({ hasError: false, error: undefined });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) return this.props.fallback;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-8 bg-slate-50 border border-slate-200 rounded-2xl h-full text-center">
|
||||
<div className="bg-rose-100 p-3 rounded-full mb-4">
|
||||
<AlertTriangle className="h-6 w-6 text-rose-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-2">Something went wrong</h3>
|
||||
<p className="text-sm text-slate-500 max-w-xs mb-6">
|
||||
{this.state.error?.message || "An unexpected error occurred while rendering this component."}
|
||||
</p>
|
||||
<Button onClick={this.resetError} variant="outline">
|
||||
<RotateCcw className="h-4 w-4 mr-2" /> Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
1164
components/GoogleAdsGenerator.tsx
Normal file
1164
components/GoogleAdsGenerator.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,19 @@ import useStore from "@/lib/store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function HistoryPanel() {
|
||||
const { history, setCurrentPrompt, clearHistory } = useStore();
|
||||
const { language, history, setCurrentPrompt, clearHistory } = useStore();
|
||||
const t = translations[language].history;
|
||||
const common = translations[language].common;
|
||||
|
||||
const handleRestore = (prompt: string) => {
|
||||
setCurrentPrompt(prompt);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
if (confirm("Are you sure you want to clear all history?")) {
|
||||
if (confirm(t.confirmClear)) {
|
||||
clearHistory();
|
||||
}
|
||||
};
|
||||
@@ -21,12 +24,12 @@ export default function HistoryPanel() {
|
||||
if (history.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6">
|
||||
<div className="text-center">
|
||||
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6 text-center">
|
||||
<div>
|
||||
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
||||
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground">No history yet</p>
|
||||
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground font-medium">{t.empty}</p>
|
||||
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
||||
Start enhancing prompts to see them here
|
||||
{t.emptyDesc}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -36,12 +39,14 @@ export default function HistoryPanel() {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6">
|
||||
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6 text-start">
|
||||
<div>
|
||||
<CardTitle className="text-base lg:text-lg">History</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">{history.length} items</CardDescription>
|
||||
<CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{history.length} {t.items}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9" title={t.clear}>
|
||||
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
|
||||
15
components/LocaleProvider.tsx
Normal file
15
components/LocaleProvider.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import useStore from "@/lib/store";
|
||||
|
||||
export default function LocaleProvider({ children }: { children: React.ReactNode }) {
|
||||
const language = useStore((state) => state.language);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language;
|
||||
document.documentElement.dir = language === "he" ? "rtl" : "ltr";
|
||||
}, [language]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
578
components/MarketResearcher.tsx
Normal file
578
components/MarketResearcher.tsx
Normal file
@@ -0,0 +1,578 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import useStore from "@/lib/store";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Search, Globe, Plus, Trash2, ShieldAlert, BarChart3, TrendingUp, Target, Rocket, Lightbulb, CheckCircle2, AlertCircle, Loader2, X, ExternalLink } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const MarketResearcher = () => {
|
||||
const { language, selectedProvider, selectedModels, apiKeys, setMarketResearchResult, marketResearchResult } = useStore();
|
||||
const t = translations[language].marketResearch;
|
||||
const common = translations[language].common;
|
||||
|
||||
const [websiteUrl, setWebsiteUrl] = useState("");
|
||||
const [additionalUrls, setAdditionalUrls] = useState<string[]>([""]);
|
||||
const [competitorUrls, setCompetitorUrls] = useState<string[]>(["", "", ""]);
|
||||
const [productMapping, setProductMapping] = useState("");
|
||||
const [specialInstructions, setSpecialInstructions] = useState("");
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [thoughtIndex, setThoughtIndex] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
|
||||
const handleAddUrl = () => setAdditionalUrls([...additionalUrls, ""]);
|
||||
const handleRemoveUrl = (index: number) => {
|
||||
const newUrls = [...additionalUrls];
|
||||
newUrls.splice(index, 1);
|
||||
setAdditionalUrls(newUrls);
|
||||
};
|
||||
|
||||
const handleAddCompetitor = () => {
|
||||
if (competitorUrls.length < 10) {
|
||||
setCompetitorUrls([...competitorUrls, ""]);
|
||||
}
|
||||
};
|
||||
const handleRemoveCompetitor = (index: number) => {
|
||||
const newUrls = [...competitorUrls];
|
||||
newUrls.splice(index, 1);
|
||||
setCompetitorUrls(newUrls);
|
||||
};
|
||||
|
||||
const validateUrls = () => {
|
||||
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
|
||||
if (!websiteUrl || !urlRegex.test(websiteUrl)) return t.invalidPrimaryUrl;
|
||||
|
||||
const validCompetitors = competitorUrls.filter(url => url.trim().length > 0);
|
||||
if (validCompetitors.length < 2) return t.minCompetitors;
|
||||
|
||||
for (const url of validCompetitors) {
|
||||
if (!urlRegex.test(url)) return `${t.invalidCompetitorUrl}: ${url}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (isProcessing) {
|
||||
setProgress(0);
|
||||
setThoughtIndex(0);
|
||||
interval = setInterval(() => {
|
||||
setProgress(prev => {
|
||||
if (prev >= 95) return prev;
|
||||
return prev + (prev < 30 ? 2 : prev < 70 ? 1 : 0.5);
|
||||
});
|
||||
}, 300);
|
||||
|
||||
const thoughtInterval = setInterval(() => {
|
||||
setThoughtIndex(prev => (prev < (t.thoughts?.length || 0) - 1 ? prev + 1 : prev));
|
||||
}, 4000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
clearInterval(thoughtInterval);
|
||||
};
|
||||
} else {
|
||||
setProgress(0);
|
||||
}
|
||||
}, [isProcessing, t.thoughts]);
|
||||
|
||||
const handleStartResearch = async () => {
|
||||
const validationError = validateUrls();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = apiKeys[selectedProvider];
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setError(null);
|
||||
setMarketResearchResult(null);
|
||||
|
||||
try {
|
||||
const filteredCompetitors = competitorUrls.filter(u => u.trim() !== "");
|
||||
const filteredAddUrls = additionalUrls.filter(u => u.trim() !== "");
|
||||
|
||||
const result = await modelAdapter.generateMarketResearch({
|
||||
websiteUrl,
|
||||
additionalUrls: filteredAddUrls,
|
||||
competitors: filteredCompetitors,
|
||||
productMapping,
|
||||
specialInstructions
|
||||
}, selectedProvider, selectedModel);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setProgress(100);
|
||||
try {
|
||||
const cleanJson = result.data.replace(/```json\s*([\s\S]*?)\s*```/i, '$1').trim();
|
||||
const parsed = JSON.parse(cleanJson);
|
||||
setMarketResearchResult({
|
||||
...parsed,
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
websiteUrl,
|
||||
additionalUrls: filteredAddUrls,
|
||||
competitors: filteredCompetitors,
|
||||
productMapping: [{ productName: productMapping || t.mainProduct, features: [] }],
|
||||
generatedAt: new Date(),
|
||||
rawContent: result.data
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to parse market research JSON:", e);
|
||||
setError(t.parseError);
|
||||
}
|
||||
} else {
|
||||
setError(result.error || t.researchFailed);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t.unexpectedError);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPriceMatrix = () => {
|
||||
if (!marketResearchResult?.priceComparisonMatrix) return null;
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b bg-slate-50/50">
|
||||
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">{t.product}</th>
|
||||
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">{t.yourPrice}</th>
|
||||
{marketResearchResult.competitors.map((comp, i) => (
|
||||
<th key={i} className="px-4 py-3 font-black text-slate-500 uppercase tracking-wider text-[10px]">{comp}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{marketResearchResult.priceComparisonMatrix.map((item, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50/30 transition-colors">
|
||||
<td className="px-4 py-4 font-bold text-slate-900">{item.product}</td>
|
||||
<td className="px-4 py-4 font-black text-indigo-600">{item.userPrice}</td>
|
||||
{marketResearchResult.competitors.map((comp) => {
|
||||
const compPrice = item.competitorPrices.find(cp => cp.competitor === comp || comp.includes(cp.competitor));
|
||||
return (
|
||||
<td key={comp} className="px-4 py-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-slate-600">{compPrice ? compPrice.price : t.notAvailable}</span>
|
||||
{compPrice?.url && (
|
||||
<a
|
||||
href={compPrice.url.startsWith('http') ? compPrice.url : `https://${compPrice.url}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-[10px] text-indigo-500 hover:text-indigo-700 font-bold transition-colors group/link"
|
||||
>
|
||||
<ExternalLink className="h-2.5 w-2.5" />
|
||||
{t.viewProduct}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFeatureTable = () => {
|
||||
if (!marketResearchResult?.featureComparisonTable) return null;
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b bg-slate-50/50">
|
||||
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">{t.feature}</th>
|
||||
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">{t.you}</th>
|
||||
{marketResearchResult.competitors.map((comp, i) => (
|
||||
<th key={i} className="px-4 py-3 font-black text-slate-500 uppercase tracking-wider text-[10px]">{comp}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{marketResearchResult.featureComparisonTable.map((item, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50/30 transition-colors">
|
||||
<td className="px-4 py-4 font-bold text-slate-900">{item.feature}</td>
|
||||
<td className="px-4 py-4">
|
||||
{typeof item.userStatus === 'boolean' ? (
|
||||
item.userStatus ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <X className="h-4 w-4 text-slate-300" />
|
||||
) : <span className="text-xs font-semibold">{item.userStatus}</span>}
|
||||
</td>
|
||||
{marketResearchResult.competitors.map((comp) => {
|
||||
const compStatus = item.competitorStatus.find(cs => cs.competitor === comp || comp.includes(cs.competitor));
|
||||
return (
|
||||
<td key={comp} className="px-4 py-4">
|
||||
{compStatus ? (
|
||||
typeof compStatus.status === 'boolean' ? (
|
||||
compStatus.status ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <X className="h-4 w-4 text-slate-300" />
|
||||
) : <span className="text-xs font-medium text-slate-600">{compStatus.status}</span>
|
||||
) : t.notAvailable}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-200">
|
||||
<Search className="h-6 w-6" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-black tracking-tight text-slate-900">{t.title}</h2>
|
||||
</div>
|
||||
<p className="text-slate-500 font-medium ml-1.5">{t.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8 items-start">
|
||||
{/* Configuration Panel */}
|
||||
<div className="xl:col-span-5 space-y-6">
|
||||
<Card className="border-slate-200/60 shadow-xl shadow-slate-200/40 overflow-hidden bg-white/80 backdrop-blur-md">
|
||||
<CardHeader className="bg-slate-50/50 border-b p-5">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" /> {t.companyProfile}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.websiteUrl}</label>
|
||||
<Input
|
||||
placeholder={t.websitePlaceholder}
|
||||
value={websiteUrl}
|
||||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||
className="bg-slate-50 border-slate-200 focus:bg-white transition-all font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600 flex justify-between items-center">
|
||||
{t.additionalUrls}
|
||||
<Button variant="ghost" size="sm" onClick={handleAddUrl} className="h-6 px-2 hover:bg-slate-100 text-[10px] font-black uppercase">
|
||||
<Plus className="h-3 w-3 mr-1" /> {t.addUrl}
|
||||
</Button>
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{additionalUrls.map((url, i) => (
|
||||
<div key={i} className="flex gap-2 group">
|
||||
<Input
|
||||
placeholder={t.urlPlaceholder}
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
const newUrls = [...additionalUrls];
|
||||
newUrls[i] = e.target.value;
|
||||
setAdditionalUrls(newUrls);
|
||||
}}
|
||||
className="bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-xs"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleRemoveUrl(i)} className="h-9 w-9 shrink-0 text-slate-400 hover:text-rose-500">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200/60 shadow-xl shadow-slate-200/40 overflow-hidden bg-white/80 backdrop-blur-md">
|
||||
<CardHeader className="bg-slate-50/50 border-b p-5">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||
<ShieldAlert className="h-4 w-4" /> {t.competitiveIntel}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600 flex justify-between items-center">
|
||||
{t.competitors}
|
||||
<Button variant="ghost" size="sm" onClick={handleAddCompetitor} disabled={competitorUrls.length >= 10} className="h-6 px-2 hover:bg-slate-100 text-[10px] font-black uppercase">
|
||||
<Plus className="h-3 w-3 mr-1" /> {t.addCompetitor}
|
||||
</Button>
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{competitorUrls.map((url, i) => (
|
||||
<div key={i} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t.competitorPlaceholder}
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
const newUrls = [...competitorUrls];
|
||||
newUrls[i] = e.target.value;
|
||||
setCompetitorUrls(newUrls);
|
||||
}}
|
||||
className="bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-xs"
|
||||
/>
|
||||
{competitorUrls.length > 2 && (
|
||||
<Button variant="ghost" size="icon" onClick={() => handleRemoveCompetitor(i)} className="h-9 w-9 shrink-0 text-slate-400 hover:text-rose-500">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.productMapping}</label>
|
||||
<Textarea
|
||||
placeholder={t.mappingPlaceholder}
|
||||
value={productMapping}
|
||||
onChange={(e) => setProductMapping(e.target.value)}
|
||||
className="min-h-[80px] bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-sm"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-400 font-medium italic">{t.mappingDesc}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.parameters}</label>
|
||||
<Textarea
|
||||
placeholder={t.parametersPlaceholder}
|
||||
value={specialInstructions}
|
||||
onChange={(e) => setSpecialInstructions(e.target.value)}
|
||||
className="min-h-[80px] bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isProcessing && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-widest">
|
||||
<span className="text-indigo-600 flex items-center gap-1.5">
|
||||
<Loader2 className="h-3 w-3 animate-spin" /> {t.analysisInProgress}
|
||||
</span>
|
||||
<span className="text-slate-400">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-slate-100 rounded-full overflow-hidden border border-slate-200/50">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-indigo-500 via-violet-500 to-indigo-500 transition-all duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-slate-900 text-white shadow-lg relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 p-2 opacity-20">
|
||||
<Rocket className="h-4 w-4 text-indigo-400 group-hover:block hidden" />
|
||||
</div>
|
||||
<h4 className="text-[9px] font-black uppercase tracking-[0.2em] text-indigo-400 mb-2 flex items-center gap-1.5">
|
||||
<span className="h-1 w-1 bg-indigo-400 rounded-full animate-pulse" /> {t.aiThoughts}
|
||||
</h4>
|
||||
<p className="text-xs font-bold leading-relaxed italic animate-in fade-in slide-in-from-left-2 duration-700">
|
||||
"{t.thoughts?.[thoughtIndex] || t.researching}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 rounded-xl bg-rose-50 border border-rose-100 flex items-start gap-3 animate-in fade-in slide-in-from-top-2">
|
||||
<AlertCircle className="h-4 w-4 text-rose-500 shrink-0 mt-0.5" />
|
||||
<p className="text-xs font-bold text-rose-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleStartResearch}
|
||||
disabled={isProcessing}
|
||||
className="w-full h-12 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white font-black uppercase tracking-widest shadow-lg shadow-indigo-100 transition-all active:scale-95 disabled:opacity-70"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
{t.researching}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="mr-2 h-5 w-5" />
|
||||
{t.generate}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Results Panel */}
|
||||
<div className="xl:col-span-7">
|
||||
{marketResearchResult ? (
|
||||
<Card className="border-slate-200/60 shadow-2xl shadow-slate-200/50 overflow-hidden bg-white group min-h-[600px]">
|
||||
<CardHeader className="bg-slate-900 text-white p-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/20 rounded-full blur-3xl -mr-32 -mt-32" />
|
||||
<div className="relative z-10 flex justify-between items-start">
|
||||
<div>
|
||||
<Badge variant="outline" className="mb-2 border-indigo-400/50 text-indigo-300 font-black uppercase tracking-widest text-[10px]">{t.marketIntelReport}</Badge>
|
||||
<CardTitle className="text-2xl font-black tracking-tight">{marketResearchResult.websiteUrl}</CardTitle>
|
||||
<CardDescription className="text-indigo-200 font-medium">{t.generatedOn} {marketResearchResult.generatedAt.toLocaleDateString()}</CardDescription>
|
||||
</div>
|
||||
<div className="p-3 rounded-2xl bg-white/10 backdrop-blur-md border border-white/20">
|
||||
<BarChart3 className="h-6 w-6 text-indigo-300" />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Tabs defaultValue="summary" className="w-full">
|
||||
<TabsList className="w-full h-14 bg-slate-50 border-b rounded-none px-6 justify-start gap-4">
|
||||
<TabsTrigger value="summary" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.summary}</TabsTrigger>
|
||||
<TabsTrigger value="pricing" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.pricing}</TabsTrigger>
|
||||
<TabsTrigger value="features" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.features}</TabsTrigger>
|
||||
<TabsTrigger value="positioning" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.positioning}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="p-6">
|
||||
<TabsContent value="summary" className="m-0 focus-visible:ring-0">
|
||||
<div className="space-y-6">
|
||||
<div className="p-5 rounded-2xl bg-indigo-50 border border-indigo-100">
|
||||
<h3 className="text-sm font-black text-indigo-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" /> {t.executiveSummary}
|
||||
</h3>
|
||||
<p className="text-sm text-indigo-900/80 leading-relaxed font-medium">
|
||||
{marketResearchResult.executiveSummary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-5 rounded-2xl border bg-emerald-50/30 border-emerald-100">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-emerald-600 mb-3 flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4" /> {t.strategicAdvantages}
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{marketResearchResult.competitiveAnalysis.advantages.map((adv, i) => (
|
||||
<li key={i} className="text-xs font-bold text-slate-700 flex gap-2">
|
||||
<span className="text-emerald-500">•</span> {adv}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-5 rounded-2xl border bg-rose-50/30 border-rose-100">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-rose-600 mb-3 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" /> {t.identifiedGaps}
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{marketResearchResult.competitiveAnalysis.disadvantages.map((dis, i) => (
|
||||
<li key={i} className="text-xs font-bold text-slate-700 flex gap-2">
|
||||
<span className="text-rose-500">•</span> {dis}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl border bg-amber-50/30 border-amber-100">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-amber-600 mb-3 flex items-center gap-2">
|
||||
<Lightbulb className="h-4 w-4" /> {t.recommendations}
|
||||
</h4>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{marketResearchResult.recommendations.map((rec, i) => (
|
||||
<li key={i} className="text-xs font-bold text-slate-700 p-3 bg-white border border-amber-100 rounded-xl shadow-sm flex items-center gap-3">
|
||||
<span className="h-6 w-6 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center text-[10px] shrink-0">{i + 1}</span>
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pricing" className="m-0 focus-visible:ring-0">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight">{t.priceMatrix}</h3>
|
||||
<Badge className="bg-slate-900 text-[10px] font-black uppercase">{t.liveMarketData}</Badge>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
||||
{renderPriceMatrix()}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="features" className="m-0 focus-visible:ring-0">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight">{t.featureBenchmarking}</h3>
|
||||
<Badge className="bg-indigo-600 text-[10px] font-black uppercase">{t.functionalAudit}</Badge>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
||||
{renderFeatureTable()}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="positioning" className="m-0 focus-visible:ring-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="p-5 rounded-2xl bg-slate-900 text-white shadow-xl">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-indigo-400 mb-3 flex items-center gap-2">
|
||||
<Target className="h-4 w-4" /> {t.marketLandscape}
|
||||
</h4>
|
||||
<p className="text-xs font-medium leading-relaxed opacity-90">
|
||||
{marketResearchResult.marketPositioning.landscape}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="p-5 rounded-2xl bg-indigo-600 text-white shadow-xl">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3 flex items-center gap-2">
|
||||
<Rocket className="h-4 w-4" /> {t.segmentationStrategy}
|
||||
</h4>
|
||||
<p className="text-xs font-medium leading-relaxed font-bold">
|
||||
{marketResearchResult.marketPositioning.segmentation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2 p-5 rounded-2xl border bg-slate-50 italic">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-2">{t.methodology}</h4>
|
||||
<p className="text-[10px] font-medium text-slate-400">
|
||||
{marketResearchResult.methodology}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="border-dashed border-2 border-slate-200 bg-slate-50/50 flex flex-col items-center justify-center p-12 min-h-[600px] text-center group">
|
||||
<div className="h-20 w-20 rounded-3xl bg-white border border-slate-100 flex items-center justify-center mb-6 shadow-sm group-hover:scale-110 group-hover:rotate-3 transition-all duration-500">
|
||||
<BarChart3 className="h-10 w-10 text-slate-300 group-hover:text-indigo-500 transition-colors" />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-400 tracking-tight group-hover:text-slate-600 transition-colors">{t.awaitingParameters}</h3>
|
||||
<p className="text-sm text-slate-400 font-medium max-w-[280px] mt-2 group-hover:text-slate-500 transition-colors">
|
||||
{t.emptyState}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketResearcher;
|
||||
@@ -8,6 +8,7 @@ import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function PRDGenerator() {
|
||||
const {
|
||||
@@ -19,6 +20,7 @@ export default function PRDGenerator() {
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
language,
|
||||
setCurrentPrompt,
|
||||
setSelectedProvider,
|
||||
setPRD,
|
||||
@@ -28,6 +30,9 @@ export default function PRDGenerator() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const t = translations[language].prdGenerator;
|
||||
const common = translations[language].common;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
||||
|
||||
@@ -73,7 +78,7 @@ export default function PRDGenerator() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter an idea to generate PRD");
|
||||
setError(t.enterIdeaError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,7 +86,7 @@ export default function PRDGenerator() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,11 +117,11 @@ export default function PRDGenerator() {
|
||||
setPRD(newPRD);
|
||||
} else {
|
||||
console.error("[PRDGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate PRD");
|
||||
setError(result.error || t.errorGenerate);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[PRDGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -131,29 +136,29 @@ export default function PRDGenerator() {
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{ id: "overview", title: "Overview & Objectives" },
|
||||
{ id: "personas", title: "User Personas & Use Cases" },
|
||||
{ id: "functional", title: "Functional Requirements" },
|
||||
{ id: "nonfunctional", title: "Non-functional Requirements" },
|
||||
{ id: "architecture", title: "Technical Architecture" },
|
||||
{ id: "metrics", title: "Success Metrics" },
|
||||
{ id: "overview", title: t.sections.overview },
|
||||
{ id: "personas", title: t.sections.personas },
|
||||
{ id: "functional", title: t.sections.functional },
|
||||
{ id: "nonfunctional", title: t.sections.nonfunctional },
|
||||
{ id: "architecture", title: t.sections.architecture },
|
||||
{ id: "metrics", title: t.sections.metrics },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
PRD Generator
|
||||
{t.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Generate comprehensive Product Requirements Document from your idea
|
||||
{t.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
@@ -169,8 +174,8 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
@@ -185,12 +190,11 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Your Idea</label>
|
||||
<Textarea
|
||||
placeholder="e.g., A task management app with real-time collaboration features"
|
||||
placeholder={t.placeholder}
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -200,7 +204,7 @@ export default function PRDGenerator() {
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -210,12 +214,12 @@ export default function PRDGenerator() {
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Generating PRD...
|
||||
{common.generating}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate PRD
|
||||
{common.generate}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -223,11 +227,11 @@ export default function PRDGenerator() {
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!prd && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Generated PRD
|
||||
{t.generatedTitle}
|
||||
</span>
|
||||
{prd && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
@@ -240,7 +244,7 @@ export default function PRDGenerator() {
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Structured requirements document ready for development
|
||||
{t.generatedDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
@@ -269,7 +273,7 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||
PRD will appear here
|
||||
{t.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,16 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||
import {
|
||||
Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings,
|
||||
AlertTriangle, Info, ChevronDown, ChevronUp, Target, Layers,
|
||||
Zap, Brain, FileCode, Bot, Search, Image, Code, Globe
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
import {
|
||||
runDiagnostics,
|
||||
detectToolCategory,
|
||||
selectTemplate,
|
||||
generateAnalysisReport,
|
||||
estimateTokens,
|
||||
type AnalysisReport,
|
||||
type DiagnosticResult,
|
||||
TOOL_CATEGORIES,
|
||||
TEMPLATES,
|
||||
type ToolCategory,
|
||||
} from "@/lib/enhance-engine";
|
||||
|
||||
const toolCategoryIcons: Record<string, React.ElementType> = {
|
||||
reasoning: Brain,
|
||||
thinking: Brain,
|
||||
openweight: Zap,
|
||||
agentic: Bot,
|
||||
ide: Code,
|
||||
fullstack: Globe,
|
||||
image: Image,
|
||||
search: Search,
|
||||
};
|
||||
|
||||
const toolCategoryNames: Record<string, string> = {
|
||||
reasoning: "Reasoning LLM",
|
||||
thinking: "Thinking LLM",
|
||||
openweight: "Open-Weight",
|
||||
agentic: "Agentic AI",
|
||||
ide: "IDE AI",
|
||||
fullstack: "Full-Stack Gen",
|
||||
image: "Image AI",
|
||||
search: "Search AI",
|
||||
};
|
||||
|
||||
type EnhanceMode = "quick" | "deep";
|
||||
|
||||
export default function PromptEnhancer() {
|
||||
const {
|
||||
language,
|
||||
currentPrompt,
|
||||
enhancedPrompt,
|
||||
selectedProvider,
|
||||
@@ -28,7 +70,17 @@ export default function PromptEnhancer() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const t = translations[language].promptEnhancer;
|
||||
const common = translations[language].common;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [toolCategory, setToolCategory] = useState<string>("reasoning");
|
||||
const [templateId, setTemplateId] = useState<string>("RTF");
|
||||
const [enhanceMode, setEnhanceMode] = useState<EnhanceMode>("deep");
|
||||
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
||||
const [diagnostics, setDiagnostics] = useState<DiagnosticResult[]>([]);
|
||||
const [analysis, setAnalysis] = useState<AnalysisReport | null>(null);
|
||||
const [autoDetected, setAutoDetected] = useState(false);
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
@@ -50,6 +102,36 @@ export default function PromptEnhancer() {
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
|
||||
const analyzePrompt = useCallback((prompt: string) => {
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
const report = generateAnalysisReport(prompt);
|
||||
|
||||
if (!autoDetected) {
|
||||
if (report.suggestedTool) setToolCategory(report.suggestedTool);
|
||||
if (report.suggestedTemplate) setTemplateId(report.suggestedTemplate.framework);
|
||||
setAutoDetected(true);
|
||||
}
|
||||
|
||||
setDiagnostics(report.diagnostics);
|
||||
setAnalysis(report);
|
||||
}, [autoDetected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setDiagnostics([]);
|
||||
setAnalysis(null);
|
||||
setAutoDetected(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
analyzePrompt(currentPrompt);
|
||||
}, 600);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [currentPrompt, analyzePrompt]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
@@ -66,7 +148,7 @@ export default function PromptEnhancer() {
|
||||
|
||||
const handleEnhance = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter a prompt to enhance");
|
||||
setError(t.enterPromptError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,29 +156,36 @@ export default function PromptEnhancer() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
|
||||
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||
const diagnosticsText = enhanceMode === "deep" && diagnostics.length > 0
|
||||
? diagnostics.filter(d => d.detected).map(d => `- ${d.pattern.name}: ${d.suggestion}`).join("\n")
|
||||
: "";
|
||||
|
||||
const options = enhanceMode === "deep"
|
||||
? { toolCategory, template: templateId.toLowerCase(), diagnostics: diagnosticsText }
|
||||
: { toolCategory: "reasoning", template: "rtf", diagnostics: "" };
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
|
||||
|
||||
console.log("[PromptEnhancer] Enhancement result:", result);
|
||||
const result = await modelAdapter.enhancePrompt(
|
||||
currentPrompt,
|
||||
selectedProvider,
|
||||
selectedModel,
|
||||
options
|
||||
);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setEnhancedPrompt(result.data);
|
||||
} else {
|
||||
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
||||
setError(result.error || "Failed to enhance prompt");
|
||||
setError(result.error || t.errorEnhance);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[PromptEnhancer] Enhancement error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorEnhance);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -114,129 +203,350 @@ export default function PromptEnhancer() {
|
||||
setCurrentPrompt("");
|
||||
setEnhancedPrompt(null);
|
||||
setError(null);
|
||||
setDiagnostics([]);
|
||||
setAnalysis(null);
|
||||
setAutoDetected(false);
|
||||
};
|
||||
|
||||
const criticalCount = diagnostics.filter(d => d.detected && d.severity === "critical").length;
|
||||
const warningCount = diagnostics.filter(d => d.detected && d.severity === "warning").length;
|
||||
|
||||
const toolEntries = Object.entries(TOOL_CATEGORIES) as [ToolCategory, typeof TOOL_CATEGORIES[ToolCategory]][];
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Input Prompt
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Enter your prompt and we'll enhance it for AI coding agents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||
{/* Left Column */}
|
||||
<div className="space-y-4">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
{t.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{t.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0">
|
||||
{/* Enhancement Mode Toggle */}
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.enhanceMode}</label>
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
variant={enhanceMode === "quick" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={cn(
|
||||
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
onClick={() => setEnhanceMode("quick")}
|
||||
className="flex-1 text-xs h-8"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
<Zap className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t.quickMode}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant={enhanceMode === "deep" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setEnhanceMode("deep")}
|
||||
className="flex-1 text-xs h-8"
|
||||
>
|
||||
<Brain className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t.deepMode}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Your Prompt</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Create a user authentication system with JWT tokens"
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
{/* Deep Mode Options */}
|
||||
{enhanceMode === "deep" && (
|
||||
<>
|
||||
{/* Target Tool */}
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.targetTool}</label>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{toolEntries.map(([catId, cat]) => {
|
||||
const Icon = toolCategoryIcons[catId] || Target;
|
||||
return (
|
||||
<Button
|
||||
key={catId}
|
||||
variant={toolCategory === catId ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => { setToolCategory(catId); setAutoDetected(false); }}
|
||||
className={cn(
|
||||
"justify-start text-xs h-8 px-2",
|
||||
toolCategory === catId && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon className="mr-1.5 h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate">{toolCategoryNames[catId]}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template Framework */}
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.templateLabel}</label>
|
||||
<select
|
||||
value={templateId}
|
||||
onChange={(e) => setTemplateId(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{TEMPLATES.map((tmpl) => (
|
||||
<option key={tmpl.framework} value={tmpl.framework}>
|
||||
{tmpl.name} — {tmpl.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* AI Provider */}
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(["qwen", "ollama", "zai", "openrouter"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={cn(
|
||||
"capitalize text-xs h-8 px-2.5",
|
||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : provider === "zai" ? "Z.AI" : "OpenRouter"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Enhancing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Enhance Prompt
|
||||
</>
|
||||
{/* Model */}
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Prompt Input */}
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||
<Textarea
|
||||
placeholder={t.placeholder}
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[180px] resize-y text-sm p-3 lg:p-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 text-xs text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
<span className="text-[10px]">{common.configApiKey}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 text-xs">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{common.generating}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="mr-1.5 h-3.5 w-3.5" />
|
||||
{enhanceMode === "deep" ? t.deepEnhance : t.title}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 text-xs px-3">
|
||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">{t.clear}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Diagnostics Card */}
|
||||
{enhanceMode === "deep" && diagnostics.length > 0 && (
|
||||
<Card className="h-fit">
|
||||
<CardHeader
|
||||
className="p-4 lg:p-6 text-start cursor-pointer select-none"
|
||||
onClick={() => setShowDiagnostics(!showDiagnostics)}
|
||||
>
|
||||
<CardTitle className="flex items-center justify-between text-sm lg:text-base">
|
||||
<span className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||
{t.diagnosticsTitle}
|
||||
{criticalCount > 0 && (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white">
|
||||
{criticalCount}
|
||||
</span>
|
||||
)}
|
||||
{warningCount > 0 && (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-amber-500 text-[10px] font-bold text-white">
|
||||
{warningCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{showDiagnostics ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</CardTitle>
|
||||
{analysis && !showDiagnostics && (
|
||||
<CardDescription className="text-xs">
|
||||
{t.promptQuality}: <span className="font-semibold">{analysis.overallScore}/100</span>
|
||||
{" — "}
|
||||
{analysis.suggestedTool ? toolCategoryNames[analysis.suggestedTool] : "Auto"}
|
||||
{" / "}
|
||||
{analysis.suggestedTemplate?.name || "RTF"}
|
||||
{" — ~"}
|
||||
{estimateTokens(currentPrompt)} {t.tokensLabel}
|
||||
</CardDescription>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="hidden sm:inline">Clear</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardHeader>
|
||||
{showDiagnostics && (
|
||||
<CardContent className="p-4 lg:p-6 pt-0 space-y-2">
|
||||
{analysis && (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span>{t.promptQuality}</span>
|
||||
<span className="font-semibold">{analysis.overallScore}/100</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-all",
|
||||
analysis.overallScore >= 70 ? "bg-green-500" : analysis.overallScore >= 40 ? "bg-amber-500" : "bg-red-500"
|
||||
)}
|
||||
style={{ width: `${analysis.overallScore}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className={cn(!enhancedPrompt && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
{diagnostics.filter(d => d.detected).map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"rounded-md p-2 text-xs",
|
||||
d.severity === "critical" && "bg-red-500/10 border border-red-500/20",
|
||||
d.severity === "warning" && "bg-amber-500/10 border border-amber-500/20",
|
||||
d.severity === "info" && "bg-blue-500/10 border border-blue-500/20"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{d.severity === "critical" ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-500 mt-0.5 flex-shrink-0" />
|
||||
) : d.severity === "warning" ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<Info className="h-3.5 w-3.5 text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium">{d.pattern.name}</span>
|
||||
<p className="text-muted-foreground mt-0.5">{d.suggestion}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{analysis && analysis.missingDimensions.length > 0 && (
|
||||
<div className="mt-3 rounded-md bg-muted/50 p-2.5">
|
||||
<p className="text-xs font-medium mb-1.5">{t.missingDimensions}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{analysis.missingDimensions.map((dim, i) => (
|
||||
<span key={i} className="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary">
|
||||
{dim}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analysis && (
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
|
||||
<span>~{estimateTokens(currentPrompt)} {t.inputTokens}</span>
|
||||
{enhancedPrompt && (
|
||||
<>
|
||||
<span>→</span>
|
||||
<span>~{estimateTokens(enhancedPrompt)} {t.outputTokens}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Output */}
|
||||
<Card className={cn("flex flex-col sticky top-4", !enhancedPrompt && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Enhanced Prompt
|
||||
{t.enhancedTitle}
|
||||
</span>
|
||||
{enhancedPrompt && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
{enhancedPrompt && (
|
||||
<>
|
||||
<span className="text-[10px] text-muted-foreground font-mono">
|
||||
~{estimateTokens(enhancedPrompt)} tok
|
||||
</span>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Professional prompt ready for coding agents
|
||||
{t.enhancedDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<CardContent className="p-4 lg:p-6 pt-0">
|
||||
{enhancedPrompt ? (
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{enhancedPrompt}</pre>
|
||||
<div className="space-y-3">
|
||||
{enhanceMode === "deep" && analysis && (
|
||||
<div className="rounded-md bg-primary/5 border border-primary/20 p-2.5 text-xs">
|
||||
<div className="flex items-center gap-1.5 mb-1 font-medium text-primary">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
{t.strategyNote}
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{t.strategyForTool
|
||||
.replace("{tool}", analysis.suggestedTool ? toolCategoryNames[analysis.suggestedTool] : "Reasoning LLM")
|
||||
.replace("{template}", analysis.suggestedTemplate?.name || "RTF")}
|
||||
{criticalCount > 0 && ` ${t.fixedIssues.replace("{count}", String(criticalCount))}`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 animate-in fade-in slide-in-from-bottom-2 duration-300 max-h-[60vh] overflow-y-auto">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||
Enhanced prompt will appear here
|
||||
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||
{t.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
||||
import { Save, Key, Server, Eye, EyeOff, CheckCircle, XCircle, Loader2, RefreshCw } from "lucide-react";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
||||
const {
|
||||
language,
|
||||
apiKeys,
|
||||
setApiKey,
|
||||
selectedProvider,
|
||||
setSelectedProvider,
|
||||
qwenTokens,
|
||||
setQwenTokens,
|
||||
apiValidationStatus,
|
||||
setApiValidationStatus,
|
||||
} = useStore();
|
||||
const t = translations[language].settings;
|
||||
const common = translations[language].common;
|
||||
|
||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
||||
const [validating, setValidating] = useState<Record<string, boolean>>({});
|
||||
const validationDebounceRef = useRef<Record<string, NodeJS.Timeout>>({});
|
||||
|
||||
const handleSave = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
|
||||
alert("API keys saved successfully!");
|
||||
alert(t.keysSaved);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,6 +54,10 @@ export default function SettingsPanel() {
|
||||
setApiKey("zai", keys.zai);
|
||||
modelAdapter.updateZaiApiKey(keys.zai);
|
||||
}
|
||||
if (keys.openrouter) {
|
||||
setApiKey("openrouter", keys.openrouter);
|
||||
modelAdapter.updateOpenRouterApiKey(keys.openrouter);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
@@ -46,12 +66,83 @@ export default function SettingsPanel() {
|
||||
if (storedTokens) {
|
||||
setQwenTokens(storedTokens);
|
||||
}
|
||||
|
||||
// Load validation status
|
||||
const storedValidation = localStorage.getItem("promptarch-api-validation");
|
||||
if (storedValidation) {
|
||||
try {
|
||||
const validation = JSON.parse(storedValidation);
|
||||
// Only use cached validation if it's less than 5 minutes old
|
||||
const now = Date.now();
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
const entries = Object.entries(validation) as Array<[string, any]>;
|
||||
for (const [provider, status] of entries) {
|
||||
if (status.lastValidated && (now - status.lastValidated) < fiveMinutes) {
|
||||
setApiValidationStatus(provider as any, status);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load validation status:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateApiKey = async (provider: "qwen" | "ollama" | "zai" | "openrouter") => {
|
||||
const key = apiKeys[provider];
|
||||
if (!key || key.trim().length === 0) {
|
||||
setApiValidationStatus(provider, { valid: false, error: "API key is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
setValidating((prev) => ({ ...prev, [provider]: true }));
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.validateConnection(provider);
|
||||
|
||||
if (result.success && result.data?.valid) {
|
||||
const status = {
|
||||
valid: true,
|
||||
lastValidated: Date.now(),
|
||||
models: result.data.models,
|
||||
};
|
||||
setApiValidationStatus(provider, status);
|
||||
|
||||
// Save to localStorage
|
||||
if (typeof window !== "undefined") {
|
||||
const storedValidation = localStorage.getItem("promptarch-api-validation");
|
||||
const allValidation = storedValidation ? JSON.parse(storedValidation) : {};
|
||||
allValidation[provider] = status;
|
||||
localStorage.setItem("promptarch-api-validation", JSON.stringify(allValidation));
|
||||
}
|
||||
} else {
|
||||
const status = {
|
||||
valid: false,
|
||||
error: result.error || "Connection failed",
|
||||
lastValidated: Date.now(),
|
||||
};
|
||||
setApiValidationStatus(provider, status);
|
||||
}
|
||||
} catch (error) {
|
||||
setApiValidationStatus(provider, {
|
||||
valid: false,
|
||||
error: error instanceof Error ? error.message : "Validation failed",
|
||||
lastValidated: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
setValidating((prev) => ({ ...prev, [provider]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleApiKeyChange = (provider: string, value: string) => {
|
||||
setApiKey(provider as "qwen" | "ollama" | "zai", value);
|
||||
|
||||
// Clear existing timeout
|
||||
if (validationDebounceRef.current[provider]) {
|
||||
clearTimeout(validationDebounceRef.current[provider]);
|
||||
}
|
||||
|
||||
// Update the service immediately
|
||||
switch (provider) {
|
||||
case "qwen":
|
||||
modelAdapter.updateQwenApiKey(value);
|
||||
@@ -62,6 +153,18 @@ export default function SettingsPanel() {
|
||||
case "zai":
|
||||
modelAdapter.updateZaiApiKey(value);
|
||||
break;
|
||||
case "openrouter":
|
||||
modelAdapter.updateOpenRouterApiKey(value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Debounce validation (500ms)
|
||||
if (value.trim().length > 0) {
|
||||
validationDebounceRef.current[provider] = setTimeout(() => {
|
||||
validateApiKey(provider as "qwen" | "ollama" | "zai");
|
||||
}, 500);
|
||||
} else {
|
||||
setApiValidationStatus(provider as any, { valid: false });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,6 +173,7 @@ export default function SettingsPanel() {
|
||||
setQwenTokens(null);
|
||||
modelAdapter.updateQwenTokens();
|
||||
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
|
||||
setApiValidationStatus("qwen", { valid: false });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,34 +182,80 @@ export default function SettingsPanel() {
|
||||
const token = await modelAdapter.startQwenOAuth();
|
||||
setQwenTokens(token);
|
||||
modelAdapter.updateQwenTokens(token);
|
||||
// Validate after OAuth
|
||||
await validateApiKey("qwen");
|
||||
} catch (error) {
|
||||
console.error("Qwen OAuth failed", error);
|
||||
window.alert(
|
||||
error instanceof Error ? error.message : "Qwen authentication failed"
|
||||
error instanceof Error ? error.message : t.qwenAuthFailed
|
||||
);
|
||||
} finally {
|
||||
setIsAuthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIndicator = (provider: "qwen" | "ollama" | "zai" | "openrouter") => {
|
||||
const status = apiValidationStatus[provider];
|
||||
|
||||
if (validating[provider]) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-blue-500">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<span className="text-[9px] font-medium">Validating...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status?.valid) {
|
||||
const timeAgo = status.lastValidated
|
||||
? `Validated ${Math.round((Date.now() - status.lastValidated) / 60000)}m ago`
|
||||
: "Connected";
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-green-600 dark:text-green-400">
|
||||
<CheckCircle className="h-3.5 w-3.5" />
|
||||
<span className="text-[9px] font-medium">{timeAgo}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status?.error && apiKeys[provider]?.trim().length > 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-red-500">
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
<span className="text-[9px] font-medium truncate max-w-[150px]" title={status.error}>
|
||||
{status.error}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad();
|
||||
return () => {
|
||||
// Clear all debounce timeouts on unmount
|
||||
Object.values(validationDebounceRef.current).forEach(timeout => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
API Configuration
|
||||
{t.apiKeys}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Configure API keys for different AI providers
|
||||
{t.apiKeysDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Qwen Code API Key
|
||||
@@ -113,37 +263,54 @@ export default function SettingsPanel() {
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.qwen ? "text" : "password"}
|
||||
placeholder="Enter your Qwen API key"
|
||||
placeholder={t.enterKey("Qwen")}
|
||||
value={apiKeys.qwen || ""}
|
||||
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
className="font-mono text-xs lg:text-sm pr-24"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
|
||||
>
|
||||
{showApiKey.qwen ? (
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://help.aliyun.com/zh/dashscope/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
{getStatusIndicator("qwen")}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
|
||||
>
|
||||
Alibaba DashScope
|
||||
</a>
|
||||
</p>
|
||||
{showApiKey.qwen ? (
|
||||
<EyeOff className="h-3 w-3" />
|
||||
) : (
|
||||
<Eye className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 lg:gap-4">
|
||||
<div className="flex-1 flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://help.aliyun.com/zh/dashscope/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Alibaba DashScope
|
||||
</a>
|
||||
</p>
|
||||
{apiKeys.qwen && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-[9px] lg:text-[10px] w-fit"
|
||||
onClick={() => validateApiKey("qwen")}
|
||||
disabled={validating.qwen}
|
||||
>
|
||||
<RefreshCw className={`h-2.5 w-2.5 mr-1 ${validating.qwen ? "animate-spin" : ""}`} />
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant={qwenTokens ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
@@ -152,20 +319,20 @@ export default function SettingsPanel() {
|
||||
disabled={isAuthLoading}
|
||||
>
|
||||
{isAuthLoading
|
||||
? "Signing in..."
|
||||
? t.signingIn
|
||||
: qwenTokens
|
||||
? "Logout from Qwen"
|
||||
: "Login with Qwen (OAuth)"}
|
||||
? t.logoutQwen
|
||||
: t.loginQwen}
|
||||
</Button>
|
||||
</div>
|
||||
{qwenTokens && (
|
||||
<p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
|
||||
✓ Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
||||
✓ {t.authenticated} ({t.expires}: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Ollama Cloud API Key
|
||||
@@ -173,39 +340,56 @@ export default function SettingsPanel() {
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.ollama ? "text" : "password"}
|
||||
placeholder="Enter your Ollama API key"
|
||||
placeholder={t.enterKey("Ollama")}
|
||||
value={apiKeys.ollama || ""}
|
||||
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
className="font-mono text-xs lg:text-sm pr-24"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
|
||||
>
|
||||
{showApiKey.ollama ? (
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
{getStatusIndicator("ollama")}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
|
||||
>
|
||||
{showApiKey.ollama ? (
|
||||
<EyeOff className="h-3 w-3" />
|
||||
) : (
|
||||
<Eye className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://ollama.com/cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
ollama.com/cloud
|
||||
</a>
|
||||
</p>
|
||||
{apiKeys.ollama && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-[9px] lg:text-[10px] w-fit"
|
||||
onClick={() => validateApiKey("ollama")}
|
||||
disabled={validating.ollama}
|
||||
>
|
||||
<RefreshCw className={`h-2.5 w-2.5 mr-1 ${validating.ollama ? "animate-spin" : ""}`} />
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://ollama.com/cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
ollama.com/cloud
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Z.AI Plan API Key
|
||||
@@ -213,72 +397,147 @@ export default function SettingsPanel() {
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.zai ? "text" : "password"}
|
||||
placeholder="Enter your Z.AI API key"
|
||||
placeholder={t.enterKey("Z.AI")}
|
||||
value={apiKeys.zai || ""}
|
||||
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
className="font-mono text-xs lg:text-sm pr-24"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
|
||||
>
|
||||
{showApiKey.zai ? (
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
{getStatusIndicator("zai")}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
|
||||
>
|
||||
{showApiKey.zai ? (
|
||||
<EyeOff className="h-3 w-3" />
|
||||
) : (
|
||||
<Eye className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://docs.z.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
docs.z.ai
|
||||
</a>
|
||||
</p>
|
||||
{apiKeys.zai && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-[9px] lg:text-[10px] w-fit"
|
||||
onClick={() => validateApiKey("zai")}
|
||||
disabled={validating.zai}
|
||||
>
|
||||
<RefreshCw className={`h-2.5 w-2.5 mr-1 ${validating.zai ? "animate-spin" : ""}`} />
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
OpenRouter API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.openrouter ? "text" : "password"}
|
||||
placeholder={t.enterKey("OpenRouter")}
|
||||
value={apiKeys.openrouter || ""}
|
||||
onChange={(e) => handleApiKeyChange("openrouter", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-24"
|
||||
/>
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
{getStatusIndicator("openrouter")}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, openrouter: !prev.openrouter }))}
|
||||
>
|
||||
{showApiKey.openrouter ? (
|
||||
<EyeOff className="h-3 w-3" />
|
||||
) : (
|
||||
<Eye className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://openrouter.ai/keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
openrouter.ai/keys
|
||||
</a>
|
||||
</p>
|
||||
{apiKeys.openrouter && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-[9px] lg:text-[10px] w-fit"
|
||||
onClick={() => validateApiKey("openrouter")}
|
||||
disabled={validating.openrouter}
|
||||
>
|
||||
<RefreshCw className={`h-2.5 w-2.5 mr-1 ${validating.openrouter ? "animate-spin" : ""}`} />
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://docs.z.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
docs.z.ai
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Save API Keys
|
||||
{t.saveKeys}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Default Provider</CardTitle>
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="text-base lg:text-lg">{t.defaultProvider}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Select your preferred AI provider
|
||||
{t.defaultProviderDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="grid gap-2 lg:gap-3">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
{(["qwen", "ollama", "zai", "openrouter"] as const).map((provider) => (
|
||||
<button
|
||||
key={provider}
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg:p-4 text-left transition-colors hover:bg-muted/50 ${selectedProvider === provider
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border"
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
|
||||
<Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider === "openrouter" ? "OpenRouter" : provider}</h3>
|
||||
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||
{provider === "qwen" && "Alibaba DashScope API"}
|
||||
{provider === "ollama" && "Ollama Cloud API"}
|
||||
{provider === "zai" && "Z.AI Plan API"}
|
||||
{provider === "qwen" && t.qwenDesc}
|
||||
{provider === "ollama" && t.ollamaDesc}
|
||||
{provider === "zai" && t.zaiDesc}
|
||||
{provider === "openrouter" && t.openrouterDesc}
|
||||
</p>
|
||||
</div>
|
||||
{selectedProvider === provider && (
|
||||
@@ -291,16 +550,16 @@ export default function SettingsPanel() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Data Privacy</CardTitle>
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Your data handling preferences
|
||||
{t.dataPrivacyTitleDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||
<p className="text-xs lg:text-sm">
|
||||
All API keys are stored locally in your browser. Your prompts are sent directly to the selected AI provider and are not stored by PromptArch.
|
||||
{t.dataPrivacyDesc}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useStore from "@/lib/store";
|
||||
import { Sparkles, FileText, ListTodo, Palette, History, Settings, Github, Menu, X } from "lucide-react";
|
||||
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone, Languages, Search, MessageSquare } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "history" | "settings";
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "market-research" | "ai-assist" | "history" | "settings";
|
||||
|
||||
interface SidebarProps {
|
||||
currentView: View;
|
||||
@@ -14,16 +15,22 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
const history = useStore((state) => state.history);
|
||||
const { language, setLanguage, history } = useStore();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const t = translations[language].sidebar;
|
||||
const common = translations[language].common;
|
||||
|
||||
const menuItems = [
|
||||
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
|
||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
||||
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: "prd" as View, label: t.prdGenerator, icon: FileText },
|
||||
{ id: "action" as View, label: t.actionPlan, icon: ListTodo },
|
||||
{ id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
|
||||
{ id: "slides" as View, label: t.slidesGen, icon: Presentation },
|
||||
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone },
|
||||
{ id: "market-research" as View, label: t.marketResearch, icon: Search },
|
||||
{ id: "history" as View, label: t.history, icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: t.settings, icon: Settings },
|
||||
];
|
||||
|
||||
const handleViewChange = (view: View) => {
|
||||
@@ -36,22 +43,22 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
<div className="border-b p-4 lg:p-6">
|
||||
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
<Menu className="h-3 w-3" />
|
||||
Back to rommark.dev
|
||||
<span>{t.backToRommark}</span>
|
||||
</a>
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
|
||||
<a href="https://github.rommark.dev/admin/PromptArch" target="_blank" rel="noopener noreferrer" className="block">
|
||||
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
|
||||
<div className="flex h-7 w-7 lg:h-8 lg:w-8 items-center justify-center rounded-lg bg-[#4285F4] text-primary-foreground text-sm lg:text-base">
|
||||
PA
|
||||
</div>
|
||||
PromptArch
|
||||
{t.title}
|
||||
</h1>
|
||||
</a>
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
|
||||
<a href="https://github.rommark.dev/admin/PromptArch" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
|
||||
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
|
||||
<span>View on GitHub</span>
|
||||
<span>{t.viewOnGithub}</span>
|
||||
</a>
|
||||
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
|
||||
Forked from <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
|
||||
{t.forkedFrom} <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +69,8 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
variant={currentView === item.id ? "default" : "ghost"}
|
||||
className={cn(
|
||||
"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)}
|
||||
>
|
||||
@@ -76,32 +84,33 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="mt-6 lg:mt-8 p-2 lg:p-3 text-[9px] lg:text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-3 lg:pt-4">
|
||||
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
|
||||
<div className="space-y-0.5 lg:space-y-1">
|
||||
<p>
|
||||
GitHub: <a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a>
|
||||
</p>
|
||||
<p>
|
||||
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
|
||||
</p>
|
||||
<p className="mt-1 lg:mt-2 text-[8px] lg:text-[9px] opacity-80">
|
||||
100% Developed using GLM 4.7 model on TRAE.AI IDE.
|
||||
</p>
|
||||
<p className="text-[8px] lg:text-[9px] opacity-80">
|
||||
Model Info: <a href="https://z.ai/subscribe?ic=R0K78RJKNW" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Learn here</a>
|
||||
</p>
|
||||
<div className="mt-4 p-2 lg:p-3 border-t border-border/50">
|
||||
<div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
|
||||
<Languages className="h-3 w-3" /> {t.language}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(["en", "ru", "he"] as const).map((lang) => (
|
||||
<Button
|
||||
key={lang}
|
||||
variant={language === lang ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-7 px-2 text-[10px] uppercase font-bold"
|
||||
onClick={() => setLanguage(lang)}
|
||||
>
|
||||
{lang}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="border-t p-3 lg:p-4 hidden lg:block">
|
||||
<div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
|
||||
<p className="font-medium text-foreground">Quick Tips</p>
|
||||
<p className="font-medium text-foreground">{t.quickTips}</p>
|
||||
<ul className="mt-1.5 lg:mt-2 space-y-0.5 lg:space-y-1">
|
||||
<li>• Use different providers for best results</li>
|
||||
<li>• Copy enhanced prompts to your AI agent</li>
|
||||
<li>• PRDs generate better action plans</li>
|
||||
<li>• {t.tip1}</li>
|
||||
<li>• {t.tip2}</li>
|
||||
<li>• {t.tip3}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,11 +121,11 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
<>
|
||||
{/* Mobile Header */}
|
||||
<div className="lg:hidden fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b bg-card px-4 py-3">
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
||||
<a href="https://github.rommark.dev/admin/PromptArch" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
|
||||
PA
|
||||
</div>
|
||||
<span className="font-bold text-lg">PromptArch</span>
|
||||
<span className="font-bold text-lg">{t.title}</span>
|
||||
</a>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
1348
components/SlidesGenerator.tsx
Normal file
1348
components/SlidesGenerator.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,13 @@ import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function UXDesignerPrompt() {
|
||||
const {
|
||||
language,
|
||||
currentPrompt,
|
||||
enhancedPrompt,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
@@ -27,6 +30,9 @@ export default function UXDesignerPrompt() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const t = translations[language].uxDesigner;
|
||||
const common = translations[language].common;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
||||
|
||||
@@ -65,7 +71,7 @@ export default function UXDesignerPrompt() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter an app description");
|
||||
setError(t.enterDescriptionError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +79,7 @@ export default function UXDesignerPrompt() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,11 +99,11 @@ export default function UXDesignerPrompt() {
|
||||
setEnhancedPrompt(result.data);
|
||||
} else {
|
||||
console.error("[UXDesignerPrompt] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate UX designer prompt");
|
||||
setError(result.error || t.errorGenerate);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[UXDesignerPrompt] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -119,20 +125,20 @@ export default function UXDesignerPrompt() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
UX Designer Prompt
|
||||
{t.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Describe your app idea and get the BEST EVER prompt for UX design
|
||||
{t.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
@@ -151,8 +157,8 @@ export default function UXDesignerPrompt() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
@@ -166,16 +172,16 @@ export default function UXDesignerPrompt() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">App Description</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||
<Textarea
|
||||
placeholder="e.g., A fitness tracking app with workout plans, nutrition tracking, and social features for sharing progress with friends"
|
||||
placeholder={t.placeholder}
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||
/>
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Describe what kind of app you want, target users, key features, and any specific design preferences.
|
||||
{t.inputDesc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -185,7 +191,7 @@ export default function UXDesignerPrompt() {
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -196,30 +202,30 @@ export default function UXDesignerPrompt() {
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Generating...
|
||||
{common.generating}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate UX Prompt
|
||||
{t.generateButton}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||
<span className="hidden sm:inline">Clear</span>
|
||||
<span className="hidden sm:inline">{translations[language].promptEnhancer.clear}</span>
|
||||
<span className="sm:hidden">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!generatedPrompt && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
<span className="hidden sm:inline">Best Ever UX Prompt</span>
|
||||
<span className="sm:hidden">UX Prompt</span>
|
||||
<span className="hidden sm:inline">{t.resultTitle}</span>
|
||||
<span className="sm:hidden">{t.uxPromptMobile}</span>
|
||||
</span>
|
||||
{generatedPrompt && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
@@ -232,17 +238,17 @@ export default function UXDesignerPrompt() {
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Comprehensive UX design prompt ready for designers
|
||||
{t.resultDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{generatedPrompt ? (
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{generatedPrompt}</pre>
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{generatedPrompt}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4">
|
||||
Your comprehensive UX designer prompt will appear here
|
||||
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4 italic">
|
||||
{t.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
36
components/ui/badge.tsx
Normal file
36
components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> { }
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
55
components/ui/tabs.tsx
Normal file
55
components/ui/tabs.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
108
lib/artifact-utils.ts
Normal file
108
lib/artifact-utils.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import JSZip from "jszip";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
export async function downloadArtifactAsZip(data: string, type: string, language: string = "html") {
|
||||
const zip = new JSZip();
|
||||
const extension = language === "html" || type === "web" || type === "app" ? "html" : (language === "typescript" || language === "tsx" ? "tsx" : "txt");
|
||||
const filename = `artifact-${Date.now()}.${extension}`;
|
||||
|
||||
// Check if data contains common multi-file structures (simple heuristic)
|
||||
// If it looks like a full project (multiple files defined in one block), we could parse it,
|
||||
// but for now we'll just save the main artifact.
|
||||
zip.file(filename, data);
|
||||
|
||||
// Add a basic README
|
||||
zip.file("README.md", `# AI Generated Artifact\n\nType: ${type}\nGenerated: ${new Date().toLocaleString()}`);
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
saveAs(content, `promptarch-artifact-${Date.now()}.zip`);
|
||||
}
|
||||
|
||||
export async function pushToGithub(
|
||||
token: string,
|
||||
repoName: string,
|
||||
files: { path: string; content: string }[],
|
||||
description: string = "Generated by PromptArch"
|
||||
) {
|
||||
const headers = {
|
||||
'Authorization': `token ${token}`,
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// 1. Check if repo exists, if not create it
|
||||
let repoData;
|
||||
const userResponse = await fetch('https://api.github.com/user', { headers });
|
||||
if (!userResponse.ok) throw new Error("Failed to authenticate with GitHub");
|
||||
const userData = await userResponse.json();
|
||||
const username = userData.login;
|
||||
|
||||
const repoCheckResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}`, { headers });
|
||||
|
||||
if (repoCheckResponse.status === 404) {
|
||||
// Create repo
|
||||
const createResponse = await fetch('https://api.github.com/user/repos', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
name: repoName,
|
||||
description,
|
||||
auto_init: true
|
||||
})
|
||||
});
|
||||
if (!createResponse.ok) throw new Error("Failed to create repository");
|
||||
repoData = await createResponse.json();
|
||||
// Wait a bit for repo to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
} else {
|
||||
repoData = await repoCheckResponse.json();
|
||||
}
|
||||
|
||||
// 2. Get latest commit SHA of default branch
|
||||
const branchResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/branches/${repoData.default_branch}`, { headers });
|
||||
const branchData = await branchResponse.json();
|
||||
const latestCommitSha = branchData.commit.sha;
|
||||
|
||||
// 3. Create a new tree
|
||||
const treeItems = files.map(file => ({
|
||||
path: file.path,
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
content: file.content
|
||||
}));
|
||||
|
||||
const treeResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/trees`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
base_tree: latestCommitSha,
|
||||
tree: treeItems
|
||||
})
|
||||
});
|
||||
const treeData = await treeResponse.json();
|
||||
|
||||
// 4. Create a new commit
|
||||
const commitResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/commits`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
message: `Update from PromptArch: ${new Date().toISOString()}`,
|
||||
tree: treeData.sha,
|
||||
parents: [latestCommitSha]
|
||||
})
|
||||
});
|
||||
const commitData = await commitResponse.json();
|
||||
|
||||
// 5. Update the reference
|
||||
const refResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/refs/heads/${repoData.default_branch}`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
sha: commitData.sha
|
||||
})
|
||||
});
|
||||
|
||||
if (!refResponse.ok) throw new Error("Failed to update branch reference");
|
||||
|
||||
return { url: repoData.html_url };
|
||||
}
|
||||
972
lib/enhance-engine.ts
Normal file
972
lib/enhance-engine.ts
Normal file
@@ -0,0 +1,972 @@
|
||||
/**
|
||||
* Prompt Enhancement Engine
|
||||
* Based on prompt-master methodology (https://github.com/nidhinjs/prompt-master)
|
||||
* Client-side prompt analysis and optimization for various AI tools
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// TYPE DEFINITIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Tool categories with different prompting requirements
|
||||
*/
|
||||
export type ToolCategory =
|
||||
| 'reasoning' // Claude, GPT-4o, Gemini - Full structure, XML tags, explicit format locks
|
||||
| 'thinking' // o1, o3, DeepSeek-R1 - Short clean instructions only, no CoT
|
||||
| 'openweight' // Llama, Mistral, Qwen - Shorter prompts, simpler structure
|
||||
| 'agentic' // Claude Code, Devin, SWE-agent - Start/target state, allowed/forbidden actions, stop conditions
|
||||
| 'ide' // Cursor, Windsurf, Copilot - File path + function + desired change + scope lock
|
||||
| 'fullstack' // Bolt, v0, Lovable - Stack spec, component boundaries, what NOT to scaffold
|
||||
| 'image' // Midjourney, DALL-E, Stable Diffusion - Subject + style + mood + lighting + negative prompts
|
||||
| 'search'; // Perplexity, SearchGPT - Mode specification, citation requirements
|
||||
|
||||
/**
|
||||
* Template frameworks for different prompt structures
|
||||
*/
|
||||
export type TemplateFramework =
|
||||
| 'RTF' // Role, Task, Format - Simple one-shot
|
||||
| 'CO-STAR' // Context, Objective, Style, Tone, Audience, Response - Professional documents
|
||||
| 'RISEN' // Role, Instructions, Steps, End Goal, Narrowing - Complex multi-step
|
||||
| 'CRISPE' // Capacity, Role, Insight, Statement, Personality, Experiment - Creative work
|
||||
| 'ChainOfThought' // Logic/math/debugging (NOT for thinking models)
|
||||
| 'FewShot' // Format-sensitive tasks
|
||||
| 'FileScope' // IDE AI editing
|
||||
| 'ReActPlusStop' // Agentic AI
|
||||
| 'VisualDescriptor'; // Image generation
|
||||
|
||||
/**
|
||||
* Severity levels for diagnostic patterns
|
||||
*/
|
||||
export type Severity = 'critical' | 'warning' | 'info';
|
||||
|
||||
/**
|
||||
* Diagnostic pattern for prompt analysis
|
||||
*/
|
||||
export interface DiagnosticPattern {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'task' | 'context' | 'format' | 'scope' | 'reasoning' | 'agentic';
|
||||
detect: (prompt: string) => boolean;
|
||||
fix: string;
|
||||
severity: Severity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from running diagnostics on a prompt
|
||||
*/
|
||||
export interface DiagnosticResult {
|
||||
pattern: DiagnosticPattern;
|
||||
detected: boolean;
|
||||
severity: Severity;
|
||||
suggestion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template structure with metadata
|
||||
*/
|
||||
export interface Template {
|
||||
name: string;
|
||||
framework: TemplateFramework;
|
||||
description: string;
|
||||
structure: string[];
|
||||
bestFor: ToolCategory[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete analysis report for a prompt
|
||||
*/
|
||||
export interface AnalysisReport {
|
||||
prompt: string;
|
||||
tokenEstimate: number;
|
||||
suggestedTool: ToolCategory | null;
|
||||
suggestedTemplate: Template | null;
|
||||
diagnostics: DiagnosticResult[];
|
||||
missingDimensions: string[];
|
||||
overallScore: number; // 0-100
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL CATEGORIES
|
||||
// ============================================================================
|
||||
|
||||
export const TOOL_CATEGORIES: Record<ToolCategory, {
|
||||
description: string;
|
||||
examples: string[];
|
||||
promptingStyle: string;
|
||||
}> = {
|
||||
reasoning: {
|
||||
description: 'Models with strong reasoning capabilities',
|
||||
examples: ['Claude', 'GPT-4o', 'Gemini'],
|
||||
promptingStyle: 'Full structure, XML tags, explicit format locks, detailed instructions'
|
||||
},
|
||||
thinking: {
|
||||
description: 'Models with built-in chain-of-thought',
|
||||
examples: ['o1', 'o3', 'DeepSeek-R1'],
|
||||
promptingStyle: 'Short clean instructions only, NO explicit CoT or step-by-step'
|
||||
},
|
||||
openweight: {
|
||||
description: 'Open-source models',
|
||||
examples: ['Llama', 'Mistral', 'Qwen'],
|
||||
promptingStyle: 'Shorter prompts, simpler structure, clear direct instructions'
|
||||
},
|
||||
agentic: {
|
||||
description: 'Autonomous coding agents',
|
||||
examples: ['Claude Code', 'Devin', 'SWE-agent'],
|
||||
promptingStyle: 'Start/target state, allowed/forbidden actions, stop conditions'
|
||||
},
|
||||
ide: {
|
||||
description: 'IDE-integrated AI assistants',
|
||||
examples: ['Cursor', 'Windsurf', 'Copilot'],
|
||||
promptingStyle: 'File path + function + desired change + scope lock'
|
||||
},
|
||||
fullstack: {
|
||||
description: 'Full-stack app builders',
|
||||
examples: ['Bolt', 'v0', 'Lovable'],
|
||||
promptingStyle: 'Stack spec, component boundaries, what NOT to scaffold'
|
||||
},
|
||||
image: {
|
||||
description: 'Image generation models',
|
||||
examples: ['Midjourney', 'DALL-E', 'Stable Diffusion'],
|
||||
promptingStyle: 'Subject + style + mood + lighting + negative prompts'
|
||||
},
|
||||
search: {
|
||||
description: 'Search-augmented AI',
|
||||
examples: ['Perplexity', 'SearchGPT'],
|
||||
promptingStyle: 'Mode specification, citation requirements, source attribution'
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TEMPLATE FRAMEWORKS
|
||||
// ============================================================================
|
||||
|
||||
export const TEMPLATES: Template[] = [
|
||||
{
|
||||
name: 'RTF (Role-Task-Format)',
|
||||
framework: 'RTF',
|
||||
description: 'Simple one-shot prompts with clear role, task, and output format',
|
||||
structure: ['Role: Who you are', 'Task: What to do', 'Format: How to output'],
|
||||
bestFor: ['reasoning', 'openweight']
|
||||
},
|
||||
{
|
||||
name: 'CO-STAR',
|
||||
framework: 'CO-STAR',
|
||||
description: 'Comprehensive framework for professional documents and complex tasks',
|
||||
structure: [
|
||||
'Context: Background information',
|
||||
'Objective: What needs to be achieved',
|
||||
'Style: Writing style and tone',
|
||||
'Tone: Emotional tone',
|
||||
'Audience: Who will read this',
|
||||
'Response: Expected output format'
|
||||
],
|
||||
bestFor: ['reasoning', 'thinking', 'openweight']
|
||||
},
|
||||
{
|
||||
name: 'RISEN',
|
||||
framework: 'RISEN',
|
||||
description: 'Multi-step complex task framework with clear end goals',
|
||||
structure: [
|
||||
'Role: AI agent identity',
|
||||
'Instructions: Task requirements',
|
||||
'Steps: Sequential actions',
|
||||
'End Goal: Success criteria',
|
||||
'Narrowing: Constraints and boundaries'
|
||||
],
|
||||
bestFor: ['reasoning', 'agentic']
|
||||
},
|
||||
{
|
||||
name: 'CRISPE',
|
||||
framework: 'CRISPE',
|
||||
description: 'Creative work framework with personality and experimentation',
|
||||
structure: [
|
||||
'Capacity: What you can do',
|
||||
'Role: Creative identity',
|
||||
'Insight: Key perspective',
|
||||
'Statement: The core request',
|
||||
'Personality: Tone and style',
|
||||
'Experiment: Creative constraints'
|
||||
],
|
||||
bestFor: ['reasoning', 'openweight']
|
||||
},
|
||||
{
|
||||
name: 'Chain of Thought',
|
||||
framework: 'ChainOfThought',
|
||||
description: 'Step-by-step reasoning for logic, math, and debugging (NOT for thinking models)',
|
||||
structure: [
|
||||
'Problem statement',
|
||||
'Step-by-step reasoning',
|
||||
'Final answer',
|
||||
'Verification'
|
||||
],
|
||||
bestFor: ['reasoning', 'openweight']
|
||||
},
|
||||
{
|
||||
name: 'Few-Shot Learning',
|
||||
framework: 'FewShot',
|
||||
description: 'Provide examples to guide format-sensitive tasks',
|
||||
structure: [
|
||||
'Task description',
|
||||
'Example 1: Input -> Output',
|
||||
'Example 2: Input -> Output',
|
||||
'Example 3: Input -> Output',
|
||||
'Actual task'
|
||||
],
|
||||
bestFor: ['reasoning', 'openweight']
|
||||
},
|
||||
{
|
||||
name: 'File-Scope Lock',
|
||||
framework: 'FileScope',
|
||||
description: 'IDE-specific editing with precise file and function targeting',
|
||||
structure: [
|
||||
'File path',
|
||||
'Function/component name',
|
||||
'Current code snippet',
|
||||
'Desired change',
|
||||
'Scope: ONLY modify X, do NOT touch Y'
|
||||
],
|
||||
bestFor: ['ide']
|
||||
},
|
||||
{
|
||||
name: 'ReAct + Stop Conditions',
|
||||
framework: 'ReActPlusStop',
|
||||
description: 'Agentic framework with explicit stopping rules',
|
||||
structure: [
|
||||
'Starting state: Current situation',
|
||||
'Target state: Desired outcome',
|
||||
'Allowed actions: What you CAN do',
|
||||
'Forbidden actions: What you CANNOT do',
|
||||
'Stop conditions: When to pause and ask',
|
||||
'Output requirements: Progress reporting'
|
||||
],
|
||||
bestFor: ['agentic']
|
||||
},
|
||||
{
|
||||
name: 'Visual Descriptor',
|
||||
framework: 'VisualDescriptor',
|
||||
description: 'Comprehensive image generation prompt structure',
|
||||
structure: [
|
||||
'Subject: Main element',
|
||||
'Style: Art style or aesthetic',
|
||||
'Mood: Emotional quality',
|
||||
'Lighting: Light source and quality',
|
||||
'Composition: Framing and perspective',
|
||||
'Colors: Color palette',
|
||||
'Negative prompts: What to exclude'
|
||||
],
|
||||
bestFor: ['image']
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// DIAGNOSTIC PATTERNS (35 Total)
|
||||
// ============================================================================
|
||||
|
||||
const TASK_PATTERNS: DiagnosticPattern[] = [
|
||||
{
|
||||
id: 'task-001',
|
||||
name: 'Vague task verb',
|
||||
description: 'Uses generic verbs like "help", "fix", "make" without specifics',
|
||||
category: 'task',
|
||||
detect: (prompt: string) => {
|
||||
const vagueVerbs = /\b(help|fix|make|improve|update|change|handle|work on)\b/i;
|
||||
const noSpecifics = !/\b(specifically|exactly|to|that|which|called|named):\b/i.test(prompt);
|
||||
return vagueVerbs.test(prompt) && noSpecifics && prompt.split(' ').length < 30;
|
||||
},
|
||||
fix: 'Replace vague verbs with specific action verbs. Instead of "fix this", use "add error handling to the login function"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'task-002',
|
||||
name: 'Two tasks in one',
|
||||
description: 'Contains multiple distinct tasks in a single prompt',
|
||||
category: 'task',
|
||||
detect: (prompt: string) => {
|
||||
const andPattern = /\b(and|also|plus|additionally)\s+[a-z]+\b/i;
|
||||
const commaTasks = /\b(create|build|fix|add|write|update)[^,.]+,[^,.]+(create|build|fix|add|write|update)/i;
|
||||
return andPattern.test(prompt) || commaTasks.test(prompt);
|
||||
},
|
||||
fix: 'Split into separate prompts. Each prompt should have ONE primary task.',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'task-003',
|
||||
name: 'No success criteria',
|
||||
description: 'Missing clear definition of when the task is complete',
|
||||
category: 'task',
|
||||
detect: (prompt: string) => {
|
||||
const successWords = /\b(done when|success criteria|complete when|should|must result|verify that|ensure that|passes when)\b/i;
|
||||
const isComplexTask = /\b(build|create|implement|develop|design|setup)\b/i.test(prompt);
|
||||
return isComplexTask && !successWords.test(prompt);
|
||||
},
|
||||
fix: 'Add explicit success criteria: "The task is complete when [specific condition is met]"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'task-004',
|
||||
name: 'Over-permissive agent',
|
||||
description: 'Gives AI too much freedom without constraints',
|
||||
category: 'task',
|
||||
detect: (prompt: string) => {
|
||||
const permissivePhrases = /\b(whatever it takes|do your best|figure it out|you decide|however you want|as you see fit)\b/i;
|
||||
return permissivePhrases.test(prompt);
|
||||
},
|
||||
fix: 'Replace open-ended permissions with specific constraints and scope boundaries.',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'task-005',
|
||||
name: 'Emotional task description',
|
||||
description: 'Uses emotional language without specific technical details',
|
||||
category: 'task',
|
||||
detect: (prompt: string) => {
|
||||
const emotionalWords = /\b(broken|mess|terrible|awful|doesn't work|horrible|stupid|hate|frustrating)\b/i;
|
||||
const noTechnicalDetails = !/\b(error|bug|line|function|file|exception|fail|crash)\b/i.test(prompt);
|
||||
return emotionalWords.test(prompt) && noTechnicalDetails;
|
||||
},
|
||||
fix: 'Replace emotional language with specific technical details: what error, what line, what behavior?',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'task-006',
|
||||
name: 'Build-the-whole-thing',
|
||||
description: 'Attempts to build an entire project in one prompt',
|
||||
category: 'task',
|
||||
detect: (prompt: string) => {
|
||||
const wholeProjectPhrases = /\b(entire app|whole project|full website|complete system|everything|end to end|from scratch)\b/i;
|
||||
return wholeProjectPhrases.test(prompt);
|
||||
},
|
||||
fix: 'Break down into smaller, iterative prompts. Start with core functionality, then add features.',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'task-007',
|
||||
name: 'Implicit reference',
|
||||
description: 'References something previously mentioned without context',
|
||||
category: 'task',
|
||||
detect: (prompt: string) => {
|
||||
const implicitRefs = /\b(the thing|that one|what we discussed|from before|the previous|like the other)\b/i;
|
||||
const noContext = prompt.split(' ').length < 50;
|
||||
return implicitRefs.test(prompt) && noContext;
|
||||
},
|
||||
fix: 'Always include full context. Replace "the thing" with specific name/description.',
|
||||
severity: 'critical'
|
||||
}
|
||||
];
|
||||
|
||||
const CONTEXT_PATTERNS: DiagnosticPattern[] = [
|
||||
{
|
||||
id: 'ctx-001',
|
||||
name: 'Assumed prior knowledge',
|
||||
description: 'Assumes AI remembers previous conversations or context',
|
||||
category: 'context',
|
||||
detect: (prompt: string) => {
|
||||
const assumptionPhrases = /\b(continue|as before|like we said|you know|from our chat|from earlier)\b/i;
|
||||
const noContextProvided = prompt.split(' ').length < 40;
|
||||
return assumptionPhrases.test(prompt) && noContextProvided;
|
||||
},
|
||||
fix: 'Include relevant context from previous work. Do not assume continuity.',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'ctx-002',
|
||||
name: 'No project context',
|
||||
description: 'Very short prompt with no domain or technology context',
|
||||
category: 'context',
|
||||
detect: (prompt: string) => {
|
||||
const wordCount = prompt.split(/\s+/).length;
|
||||
const hasTech = /\b(javascript|python|react|api|database|server|frontend|backend|mobile|web)\b/i;
|
||||
return wordCount < 15 && !hasTech.test(prompt);
|
||||
},
|
||||
fix: 'Add project context: technology stack, domain, and what you\'re building.',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'ctx-003',
|
||||
name: 'Forgotten stack',
|
||||
description: 'Tech-agnostic prompt that implies an existing project',
|
||||
category: 'context',
|
||||
detect: (prompt: string) => {
|
||||
const projectWords = /\b(add to|update the|change the|modify the|existing|current)\b/i;
|
||||
const noTechStack = !/\b(javascript|typescript|python|java|rust|go|react|vue|angular|node|django|rails)\b/i.test(prompt);
|
||||
return projectWords.test(prompt) && noTechStack;
|
||||
},
|
||||
fix: 'Specify your technology stack: language, framework, and key dependencies.',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'ctx-004',
|
||||
name: 'Hallucination invite',
|
||||
description: 'Asks for general knowledge that may not exist',
|
||||
category: 'context',
|
||||
detect: (prompt: string) => {
|
||||
const hallucinationPhrases = /\b(what do experts say|what is commonly known|generally accepted|most people think|typical approach)\b/i;
|
||||
return hallucinationPhrases.test(prompt);
|
||||
},
|
||||
fix: 'Ask for specific sources or provide source material. Avoid general "what do X think" questions.',
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: 'ctx-005',
|
||||
name: 'Undefined audience',
|
||||
description: 'User-facing output without audience specification',
|
||||
category: 'context',
|
||||
detect: (prompt: string) => {
|
||||
const userFacing = /\b(write|create|generate|draft)\s+(content|message|email|copy|text|documentation)\b/i;
|
||||
const noAudience = !/\b(for|audience|target|reader|user|customer|stakeholder)\b/i.test(prompt);
|
||||
return userFacing.test(prompt) && noAudience;
|
||||
},
|
||||
fix: 'Specify who will read this output: "Write for [audience] who [context]"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'ctx-006',
|
||||
name: 'No prior failures',
|
||||
description: 'Complex task without mentioning what was tried before',
|
||||
category: 'context',
|
||||
detect: (prompt: string) => {
|
||||
const complexTask = /\b(debug|fix|solve|resolve|implement|build|create)\b/i;
|
||||
const noPriorAttempts = !/\b(tried|attempted|already|previous|before|not working|failed)\b/i.test(prompt);
|
||||
const isLongPrompt = prompt.split(' ').length > 20;
|
||||
return complexTask.test(prompt) && noPriorAttempts && isLongPrompt;
|
||||
},
|
||||
fix: 'Mention what you\'ve already tried: "I tried X but got Y error. Now..."',
|
||||
severity: 'info'
|
||||
}
|
||||
];
|
||||
|
||||
const FORMAT_PATTERNS: DiagnosticPattern[] = [
|
||||
{
|
||||
id: 'fmt-001',
|
||||
name: 'Missing output format',
|
||||
description: 'No specification of how output should be structured',
|
||||
category: 'format',
|
||||
detect: (prompt: string) => {
|
||||
const formatKeywords = /\b(list|table|json|markdown|bullet|paragraph|csv|html|code|steps)\b/i;
|
||||
const outputKeywords = /\b(output|return|format as|in the form of|structure)\b/i;
|
||||
return !formatKeywords.test(prompt) && !outputKeywords.test(prompt);
|
||||
},
|
||||
fix: 'Specify output format: "Return as a bulleted list" or "Output as JSON"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'fmt-002',
|
||||
name: 'Implicit length',
|
||||
description: 'Uses length terms without specific counts',
|
||||
category: 'format',
|
||||
detect: (prompt: string) => {
|
||||
const vagueLength = /\b(summary|description|overview|brief|short|long|detailed)\b/i;
|
||||
const noSpecificLength = !/\b(\d+\s*(words?|sentences?|paragraphs?)|under\s*\d+|max\s*\d+)\b/i.test(prompt);
|
||||
return vagueLength.test(prompt) && noSpecificLength;
|
||||
},
|
||||
fix: 'Be specific: "Write 2-3 sentences" or "Keep under 100 words"',
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: 'fmt-003',
|
||||
name: 'No role assignment',
|
||||
description: 'Long prompt without specifying who AI should be',
|
||||
category: 'format',
|
||||
detect: (prompt: string) => {
|
||||
const wordCount = prompt.split(/\s+/).length;
|
||||
const roleKeywords = /\b(act as|you are|role|persona|expert|specialist|professional|engineer|developer|analyst)\b/i;
|
||||
return wordCount > 50 && !roleKeywords.test(prompt);
|
||||
},
|
||||
fix: 'Add role assignment: "Act as a [role] with [expertise]"',
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: 'fmt-004',
|
||||
name: 'Vague aesthetic',
|
||||
description: 'Design-related prompt without specific visual direction',
|
||||
category: 'format',
|
||||
detect: (prompt: string) => {
|
||||
const vagueAesthetic = /\b(professional|clean|modern|nice|good looking|beautiful|sleek)\b/i;
|
||||
const noVisualSpecs = !/\b(colors?|fonts?|spacing|layout|style|theme|design system)\b/i.test(prompt);
|
||||
return vagueAesthetic.test(prompt) && noVisualSpecs;
|
||||
},
|
||||
fix: 'Specify visual details: colors, typography, spacing, specific design reference.',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'fmt-005',
|
||||
name: 'No negative prompts for image',
|
||||
description: 'Image generation without exclusion criteria',
|
||||
category: 'format',
|
||||
detect: (prompt: string) => {
|
||||
const imageKeywords = /\b(image|photo|picture|illustration|generate|create art|midjourney|dall-e)\b/i;
|
||||
const noNegative = !/\b(negative|exclude|avoid|without|no|not)\b/i.test(prompt);
|
||||
return imageKeywords.test(prompt) && noNegative;
|
||||
},
|
||||
fix: 'Add negative prompts: "Negative: blurry, low quality, distorted"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'fmt-006',
|
||||
name: 'Prose for Midjourney',
|
||||
description: 'Long descriptive sentences instead of keyword-style prompts',
|
||||
category: 'format',
|
||||
detect: (prompt: string) => {
|
||||
const longSentences = prompt.split(/[.!?]/).filter(s => s.trim().split(' ').length > 10).length > 0;
|
||||
const imageKeywords = /\b(image|photo|art|illustration|midjourney|dall-e|stable diffusion)\b/i;
|
||||
return imageKeywords.test(prompt) && longSentences;
|
||||
},
|
||||
fix: 'Use keyword-style prompts: "Subject, style, mood, lighting, --ar 16:9"',
|
||||
severity: 'warning'
|
||||
}
|
||||
];
|
||||
|
||||
const SCOPE_PATTERNS: DiagnosticPattern[] = [
|
||||
{
|
||||
id: 'scp-001',
|
||||
name: 'No scope boundary',
|
||||
description: 'Missing specific scope constraints',
|
||||
category: 'scope',
|
||||
detect: (prompt: string) => {
|
||||
const scopeWords = /\b(only|just|specifically|exactly|limit|restrict)\b/i;
|
||||
const hasFilePath = /\/[\w.]+/.test(prompt) || /\b[\w-]+\.(js|ts|py|java|go|rs|cpp|c|h)\b/i;
|
||||
const hasFunction = /\b(function|method|class|component)\s+\w+/i;
|
||||
return !scopeWords.test(prompt) && !hasFilePath && !hasFunction;
|
||||
},
|
||||
fix: 'Add scope boundary: "Only modify X, do NOT touch Y"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'scp-002',
|
||||
name: 'No stack constraints',
|
||||
description: 'Technical task without version specifications',
|
||||
category: 'scope',
|
||||
detect: (prompt: string) => {
|
||||
const techTask = /\b(build|create|implement|setup|install|use|add)\s+(\w+\s+){0,3}(app|api|server|database|system)\b/i;
|
||||
const noVersion = !/\b(version|v\d+|\d+\.\d+|specifically|exactly)\b/i.test(prompt);
|
||||
return techTask.test(prompt) && noVersion;
|
||||
},
|
||||
fix: 'Specify versions: "Use React 18 with TypeScript 5"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'scp-003',
|
||||
name: 'No stop condition for agents',
|
||||
description: 'Agentic task without explicit stopping rules',
|
||||
category: 'scope',
|
||||
detect: (prompt: string) => {
|
||||
const agentKeywords = /\b(agent|autonomous|run this|execute|iterate|keep going)\b/i;
|
||||
const noStop = !/\b(stop|pause|ask me|check in|before continuing|confirm)\b/i.test(prompt);
|
||||
return agentKeywords.test(prompt) && noStop;
|
||||
},
|
||||
fix: 'Add stop conditions: "Stop and ask before deleting files" or "Pause after each major step"',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'scp-004',
|
||||
name: 'No file path for IDE',
|
||||
description: 'IDE editing without file specification',
|
||||
category: 'scope',
|
||||
detect: (prompt: string) => {
|
||||
const editKeywords = /\b(update|fix|change|modify|edit|refactor)\b/i;
|
||||
const hasPath = /\/[\w./-]+|\b[\w-]+\.(js|ts|jsx|tsx|py|java|go|rs|cpp|c|h|css|html|json)\b/i;
|
||||
return editKeywords.test(prompt) && !hasPath;
|
||||
},
|
||||
fix: 'Always include file path: "Update src/components/Header.tsx"',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'scp-005',
|
||||
name: 'Wrong template',
|
||||
description: 'Template mismatch for the target tool',
|
||||
category: 'scope',
|
||||
detect: (prompt: string) => {
|
||||
// Detect if using complex structure for thinking models
|
||||
const thinkingModel = /\b(o1|o3|deepseek.*r1|thinking)\b/i;
|
||||
const complexStructure = /\b(step by step|think through|reasoning|<thinking>|chain of thought)\b/i;
|
||||
return thinkingModel.test(prompt) && complexStructure.test(prompt);
|
||||
},
|
||||
fix: 'For thinking models (o1, o3, R1), use short clean instructions without explicit CoT.',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'scp-006',
|
||||
name: 'Pasting codebase',
|
||||
description: 'Extremely long prompt suggesting codebase paste',
|
||||
category: 'scope',
|
||||
detect: (prompt: string) => {
|
||||
const wordCount = prompt.split(/\s+/).length;
|
||||
const multipleFiles = (prompt.match(/```/g) || []).length > 4;
|
||||
return wordCount > 500 || multipleFiles;
|
||||
},
|
||||
fix: 'Use file paths and references instead of pasting entire files. Or use an IDE AI tool.',
|
||||
severity: 'warning'
|
||||
}
|
||||
];
|
||||
|
||||
const REASONING_PATTERNS: DiagnosticPattern[] = [
|
||||
{
|
||||
id: 'rsn-001',
|
||||
name: 'No CoT for logic',
|
||||
description: 'Complex logic task without step-by-step instructions',
|
||||
category: 'reasoning',
|
||||
detect: (prompt: string) => {
|
||||
const logicKeywords = /\b(compare|analyze|which is better|debug|why does|explain why|how does|verify)\b/i;
|
||||
const noCoT = !/\b(step by step|walk through|reasoning|think through|first|then|finally)\b/i.test(prompt);
|
||||
return logicKeywords.test(prompt) && noCoT;
|
||||
},
|
||||
fix: 'Add "Step by step" or "Walk through your reasoning" for logic tasks.',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'rsn-002',
|
||||
name: 'CoT on reasoning models',
|
||||
description: 'Explicit CoT instructions for thinking models',
|
||||
category: 'reasoning',
|
||||
detect: (prompt: string) => {
|
||||
const thinkingModel = /\b(o1|o3|deepseek.*r1)\b/i;
|
||||
const explicitCoT = /\b(step by step|think through|<thinking>|reasoning process|show your work)\b/i;
|
||||
return thinkingModel.test(prompt) && explicitCoT.test(prompt);
|
||||
},
|
||||
fix: 'Remove explicit CoT instructions. Thinking models have built-in reasoning.',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'rsn-003',
|
||||
name: 'Inter-session memory',
|
||||
description: 'Assumes AI remembers across separate sessions',
|
||||
category: 'reasoning',
|
||||
detect: (prompt: string) => {
|
||||
const memoryPhrases = /\b(you already know|remember|from our conversation|we discussed|earlier we|as mentioned)\b/i;
|
||||
return memoryPhrases.test(prompt);
|
||||
},
|
||||
fix: 'AI does not remember between sessions. Include all necessary context.',
|
||||
severity: 'info'
|
||||
},
|
||||
{
|
||||
id: 'rsn-004',
|
||||
name: 'Contradicting prior',
|
||||
description: 'Explicit contradiction of previous instructions',
|
||||
category: 'reasoning',
|
||||
detect: (prompt: string) => {
|
||||
const contradictionPhrases = /\b(actually|wait|ignore what i said|forget that|never mind|scratch that)\b/i;
|
||||
return contradictionPhrases.test(prompt);
|
||||
},
|
||||
fix: 'State corrections clearly: "Correction: Replace X with Y"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'rsn-005',
|
||||
name: 'No grounding rule',
|
||||
description: 'Factual task without certainty constraints',
|
||||
category: 'reasoning',
|
||||
detect: (prompt: string) => {
|
||||
const factualTask = /\b(summarize|what is|tell me about|explain|list|research|find)\b/i;
|
||||
const noGrounding = !/\b(if unsure|don't hallucinate|only if certain|say i don't know|stick to)\b/i.test(prompt);
|
||||
return factualTask.test(prompt) && noGrounding && prompt.split(' ').length > 10;
|
||||
},
|
||||
fix: 'Add grounding: "If uncertain, say so rather than guessing"',
|
||||
severity: 'info'
|
||||
}
|
||||
];
|
||||
|
||||
const AGENTIC_PATTERNS: DiagnosticPattern[] = [
|
||||
{
|
||||
id: 'agt-001',
|
||||
name: 'No starting state',
|
||||
description: 'Build/create task without current state description',
|
||||
category: 'agentic',
|
||||
detect: (prompt: string) => {
|
||||
const buildKeywords = /\b(build|create|set up|implement|develop|make)\b/i;
|
||||
const currentState = !/\b(currently|existing|now|currently have|right now|starting from)\b/i.test(prompt);
|
||||
return buildKeywords.test(prompt) && currentState;
|
||||
},
|
||||
fix: 'Describe starting state: "Currently I have X. I want to reach Y."',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'agt-002',
|
||||
name: 'No target state',
|
||||
description: 'Agentic task without explicit deliverable',
|
||||
category: 'agentic',
|
||||
detect: (prompt: string) => {
|
||||
const vagueCompletion = /\b(work on this|handle this|do this|take care of)\b/i;
|
||||
const noTarget = !/\b(result should|final output|deliverable|end with|complete when)\b/i.test(prompt);
|
||||
return vagueCompletion.test(prompt) && noTarget;
|
||||
},
|
||||
fix: 'Specify target state: "The final result should be [specific outcome]"',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'agt-003',
|
||||
name: 'Silent agent',
|
||||
description: 'Multi-step task without progress reporting requirements',
|
||||
category: 'agentic',
|
||||
detect: (prompt: string) => {
|
||||
const multiStep = /\b(then|next|after that|first|second|finally)\b/i;
|
||||
const noOutput = !/\b(show me|report|output|print|log|display progress|tell me)\b/i.test(prompt);
|
||||
return multiStep.test(prompt) && noOutput;
|
||||
},
|
||||
fix: 'Add output requirements: "Report progress after each step"',
|
||||
severity: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'agt-004',
|
||||
name: 'Unlocked filesystem',
|
||||
description: 'Agentic task without file access restrictions',
|
||||
category: 'agentic',
|
||||
detect: (prompt: string) => {
|
||||
const agentKeywords = /\b(agent|autonomous|run|execute|implement|build|create)\b/i;
|
||||
const noRestrictions = !/\b(only touch|don't modify|never delete|restrict to|scope|limit)\b/i.test(prompt);
|
||||
return agentKeywords.test(prompt) && noRestrictions;
|
||||
},
|
||||
fix: 'Add file restrictions: "Only modify files in X, never touch Y"',
|
||||
severity: 'critical'
|
||||
},
|
||||
{
|
||||
id: 'agt-005',
|
||||
name: 'No review trigger',
|
||||
description: 'Agentic task without approval checkpoints',
|
||||
category: 'agentic',
|
||||
detect: (prompt: string) => {
|
||||
const riskyActions = /\b(delete|remove|overwrite|deploy|publish|submit|merge)\b/i;
|
||||
const noReview = !/\b(ask before|confirm|review|approve|check with me)\b/i.test(prompt);
|
||||
return riskyActions.test(prompt) && noReview;
|
||||
},
|
||||
fix: 'Add review triggers: "Ask before deleting any files" or "Confirm before deploying"',
|
||||
severity: 'critical'
|
||||
}
|
||||
];
|
||||
|
||||
// Combine all patterns
|
||||
export const ALL_PATTERNS: DiagnosticPattern[] = [
|
||||
...TASK_PATTERNS,
|
||||
...CONTEXT_PATTERNS,
|
||||
...FORMAT_PATTERNS,
|
||||
...SCOPE_PATTERNS,
|
||||
...REASONING_PATTERNS,
|
||||
...AGENTIC_PATTERNS
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// CORE FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Auto-detect the target AI tool category based on prompt content
|
||||
*/
|
||||
export function detectToolCategory(prompt: string): ToolCategory | null {
|
||||
const p = prompt.toLowerCase();
|
||||
|
||||
// Check for specific tool mentions
|
||||
if (/(claude|gpt-4|gemini|gpt4)/i.test(prompt)) return 'reasoning';
|
||||
if (/(o1|o3|deepseek.*r1|thinking.*model)/i.test(prompt)) return 'thinking';
|
||||
if (/(llama|mistral|qwen|open.*weight|local.*model)/i.test(prompt)) return 'openweight';
|
||||
if (/(claude code|devin|swe.*agent|autonomous.*agent)/i.test(prompt)) return 'agentic';
|
||||
if (/(cursor|windsurf|copilot|ide.*ai|editor.*ai)/i.test(prompt)) return 'ide';
|
||||
if (/(bolt|v0|lovable|fullstack.*ai|app.*builder)/i.test(prompt)) return 'fullstack';
|
||||
if (/(midjourney|dall.?e|stable diffusion|image.*generate|create.*image|generate.*art)/i.test(prompt)) return 'image';
|
||||
if (/(perplexity|searchgpt|search.*ai|research.*mode)/i.test(prompt)) return 'search';
|
||||
|
||||
// Infer from content patterns
|
||||
if (/\.(js|ts|py|java|go|rs|cpp|c|h)\b/.test(prompt) && /\b(update|fix|change|modify)\b/.test(p)) return 'ide';
|
||||
if (/\b(build|create|set up|implement).*\b(app|api|server|system)\b/.test(p) && /\b(stop|pause|ask before)\b/.test(p)) return 'agentic';
|
||||
if (/\b(step by step|<thinking>|chain of thought|reasoning)\b/.test(p)) return 'reasoning';
|
||||
if (/\b(image|photo|art|illustration)\b/.test(p) && /\b(style|mood|lighting)\b/.test(p)) return 'image';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the best template based on tool category and prompt analysis
|
||||
*/
|
||||
export function selectTemplate(prompt: string, toolCategory: ToolCategory | null): Template | null {
|
||||
const p = prompt.toLowerCase();
|
||||
|
||||
// Image generation
|
||||
if (toolCategory === 'image' || /\b(image|photo|art|illustration|midjourney|dall.?e)\b/.test(p)) {
|
||||
return TEMPLATES.find(t => t.framework === 'VisualDescriptor') || null;
|
||||
}
|
||||
|
||||
// IDE editing
|
||||
if (toolCategory === 'ide' || (/\.(js|ts|py|java|go|rs)\b/.test(prompt) && /\b(update|fix|modify)\b/.test(p))) {
|
||||
return TEMPLATES.find(t => t.framework === 'FileScope') || null;
|
||||
}
|
||||
|
||||
// Agentic tasks
|
||||
if (toolCategory === 'agentic' || /\b(build|create|set up).*\b(stop|pause|ask before)\b/.test(p)) {
|
||||
return TEMPLATES.find(t => t.framework === 'ReActPlusStop') || null;
|
||||
}
|
||||
|
||||
// Complex multi-step tasks
|
||||
if (/\b(step|then|next|after|first|second|finally)\b/.test(p) && p.split(' ').length > 30) {
|
||||
return TEMPLATES.find(t => t.framework === 'RISEN') || null;
|
||||
}
|
||||
|
||||
// Logic/debugging tasks
|
||||
if (/\b(debug|compare|analyze|which is better|why does|verify)\b/.test(p)) {
|
||||
if (toolCategory !== 'thinking') {
|
||||
return TEMPLATES.find(t => t.framework === 'ChainOfThought') || null;
|
||||
}
|
||||
}
|
||||
|
||||
// Professional documents
|
||||
if (/\b(documentation|report|proposal|spec|requirements)\b/.test(p) && p.split(' ').length > 40) {
|
||||
return TEMPLATES.find(t => t.framework === 'CO-STAR') || null;
|
||||
}
|
||||
|
||||
// Creative work
|
||||
if (/\b(creative|design|story|narrative|brand|voice)\b/.test(p)) {
|
||||
return TEMPLATES.find(t => t.framework === 'CRISPE') || null;
|
||||
}
|
||||
|
||||
// Format-sensitive tasks
|
||||
if (/\b(example|sample|format|pattern|template)\b/.test(p)) {
|
||||
return TEMPLATES.find(t => t.framework === 'FewShot') || null;
|
||||
}
|
||||
|
||||
// Default to RTF for simple prompts
|
||||
if (p.split(' ').length < 50) {
|
||||
return TEMPLATES.find(t => t.framework === 'RTF') || null;
|
||||
}
|
||||
|
||||
// Default for longer prompts
|
||||
return TEMPLATES.find(t => t.framework === 'CO-STAR') || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all diagnostic patterns on a prompt
|
||||
*/
|
||||
export function runDiagnostics(prompt: string): DiagnosticResult[] {
|
||||
const results: DiagnosticResult[] = [];
|
||||
|
||||
for (const pattern of ALL_PATTERNS) {
|
||||
const detected = pattern.detect(prompt);
|
||||
if (detected) {
|
||||
results.push({
|
||||
pattern,
|
||||
detected: true,
|
||||
severity: pattern.severity,
|
||||
suggestion: pattern.fix
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by severity (critical first)
|
||||
const severityOrder = { critical: 0, warning: 1, info: 2 };
|
||||
results.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate token count (rough approximation: ~0.75 words per token)
|
||||
*/
|
||||
export function estimateTokens(prompt: string): number {
|
||||
const wordCount = prompt.split(/\s+/).length;
|
||||
return Math.ceil(wordCount * 0.75);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify missing dimensions from a prompt
|
||||
*/
|
||||
export function identifyMissingDimensions(prompt: string): string[] {
|
||||
const missing: string[] = [];
|
||||
const p = prompt.toLowerCase();
|
||||
|
||||
// Check for common dimensions
|
||||
if (!/\b(act as|you are|role|expert|specialist)\b/i.test(prompt)) {
|
||||
missing.push('Role/Identity');
|
||||
}
|
||||
|
||||
if (!/\b(context|background|project|currently working)\b/i.test(prompt)) {
|
||||
missing.push('Context');
|
||||
}
|
||||
|
||||
if (!/\b(format|output|return as|structure)\b/i.test(prompt)) {
|
||||
missing.push('Output Format');
|
||||
}
|
||||
|
||||
if (!/\b(success|complete when|done when|verify|ensure)\b/i.test(prompt)) {
|
||||
missing.push('Success Criteria');
|
||||
}
|
||||
|
||||
if (!/\b(only|just|limit|restrict|scope)\b/i.test(prompt) && prompt.split(' ').length > 20) {
|
||||
missing.push('Scope Boundaries');
|
||||
}
|
||||
|
||||
if (!/\b(javascript|python|react|node|typescript|java|rust|go)\b/i.test(prompt) &&
|
||||
/\b(code|function|class|app|api)\b/i.test(prompt)) {
|
||||
missing.push('Technology Stack');
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overall prompt quality score (0-100)
|
||||
*/
|
||||
export function calculateScore(diagnostics: DiagnosticResult[], missingDimensions: string[]): number {
|
||||
let score = 100;
|
||||
|
||||
// Deduct for diagnostics
|
||||
for (const d of diagnostics) {
|
||||
switch (d.severity) {
|
||||
case 'critical': score -= 15; break;
|
||||
case 'warning': score -= 8; break;
|
||||
case 'info': score -= 3; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Deduct for missing dimensions
|
||||
score -= missingDimensions.length * 5;
|
||||
|
||||
return Math.max(0, Math.min(100, score));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate comprehensive analysis report
|
||||
*/
|
||||
export function generateAnalysisReport(prompt: string): AnalysisReport {
|
||||
const suggestedTool = detectToolCategory(prompt);
|
||||
const suggestedTemplate = selectTemplate(prompt, suggestedTool);
|
||||
const diagnostics = runDiagnostics(prompt);
|
||||
const missingDimensions = identifyMissingDimensions(prompt);
|
||||
const tokenEstimate = estimateTokens(prompt);
|
||||
const overallScore = calculateScore(diagnostics, missingDimensions);
|
||||
|
||||
return {
|
||||
prompt,
|
||||
tokenEstimate,
|
||||
suggestedTool,
|
||||
suggestedTemplate,
|
||||
diagnostics,
|
||||
missingDimensions,
|
||||
overallScore
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable tool category description
|
||||
*/
|
||||
export function getToolDescription(category: ToolCategory): string {
|
||||
return TOOL_CATEGORIES[category].description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prompting style for a tool category
|
||||
*/
|
||||
export function getPromptingStyle(category: ToolCategory): string {
|
||||
return TOOL_CATEGORIES[category].promptingStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patterns by category
|
||||
*/
|
||||
export function getPatternsByCategory(category: DiagnosticPattern['category']): DiagnosticPattern[] {
|
||||
return ALL_PATTERNS.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pattern by ID
|
||||
*/
|
||||
export function getPatternById(id: string): DiagnosticPattern | undefined {
|
||||
return ALL_PATTERNS.find(p => p.id === id);
|
||||
}
|
||||
571
lib/export-utils.ts
Normal file
571
lib/export-utils.ts
Normal file
@@ -0,0 +1,571 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import { GoogleAdsResult, MagicWandResult } from "../types";
|
||||
|
||||
export const downloadFile = (filename: string, content: any, contentType: string) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const blob = content instanceof Blob ? content : new Blob([content], { type: contentType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export const generateGoogleAdsCSV = (googleAds?: any, magic?: any): string => {
|
||||
const rows: string[][] = [];
|
||||
|
||||
if (googleAds) {
|
||||
rows.push(["GOOGLE ADS STRATEGY REPORT"]);
|
||||
rows.push(["Generated at", new Date().toLocaleString()]);
|
||||
rows.push(["Website", googleAds.websiteUrl || 'N/A']);
|
||||
rows.push([]);
|
||||
|
||||
const kw = googleAds.keywords;
|
||||
if (kw) {
|
||||
rows.push(["KEYWORD RESEARCH"]);
|
||||
rows.push(["Type", "Keyword", "CPC", "Volume", "Competition"]);
|
||||
if (Array.isArray(kw.primary)) kw.primary.forEach((k: any) => rows.push(["Primary", String(k?.keyword || ''), String(k?.cpc || ''), String(k?.searchVolume || ''), String(k?.competition || '')]));
|
||||
if (Array.isArray(kw.longTail)) kw.longTail.forEach((k: any) => rows.push(["Long-tail", String(k?.keyword || ''), String(k?.cpc || ''), String(k?.searchVolume || ''), String(k?.competition || '')]));
|
||||
if (Array.isArray(kw.negative)) kw.negative.forEach((k: any) => rows.push(["Negative", String(k?.keyword || ''), "", "", String(k?.competition || '')]));
|
||||
rows.push([]);
|
||||
}
|
||||
|
||||
const ads = googleAds.adCopies;
|
||||
if (Array.isArray(ads)) {
|
||||
rows.push(["AD COPIES"]);
|
||||
rows.push(["Variation", "Headlines", "Descriptions", "CTA", "Optimized", "Positioning"]);
|
||||
ads.forEach((ad: any, i: number) => {
|
||||
const hl = Array.isArray(ad.headlines) ? ad.headlines.join(' | ') : '';
|
||||
const ds = Array.isArray(ad.descriptions) ? ad.descriptions.join(' | ') : '';
|
||||
rows.push([`Var ${i + 1}`, hl, ds, String(ad?.callToAction || ''), String(ad?.mobileOptimized || 'false'), String(ad?.positioning || '')]);
|
||||
});
|
||||
rows.push([]);
|
||||
}
|
||||
|
||||
const camps = googleAds.campaigns;
|
||||
if (Array.isArray(camps)) {
|
||||
rows.push(["CAMPAIGN STRUCTURE"]);
|
||||
rows.push(["Name", "Type", "Budget", "Locations", "Targeting", "Bidding"]);
|
||||
camps.forEach((c: any) => {
|
||||
const t = c.targeting;
|
||||
const locs = (t && Array.isArray(t.locations)) ? t.locations.join('; ') : 'All';
|
||||
const demos = (t && t.demographics) ? `Age: ${t.demographics.age?.join(', ') || 'Any'}; Gender: ${t.demographics.gender?.join(', ') || 'Any'}` : 'All';
|
||||
rows.push([String(c.name || ''), String(c.type || ''), `${c?.budget?.daily || 0} ${c?.budget?.currency || ''}`, locs, demos, String(c.biddingStrategy || '')]);
|
||||
});
|
||||
rows.push([]);
|
||||
}
|
||||
}
|
||||
|
||||
if (magic) {
|
||||
rows.push(["MARKET INTELLIGENCE"]);
|
||||
const ma = magic.marketAnalysis;
|
||||
if (ma) {
|
||||
rows.push(["Size", String(ma.industrySize || '')]);
|
||||
rows.push(["Growth", String(ma.growthRate || '')]);
|
||||
rows.push(["Trends", Array.isArray(ma.marketTrends) ? ma.marketTrends.join('; ') : '']);
|
||||
rows.push(["Competitors", Array.isArray(ma.topCompetitors) ? ma.topCompetitors.join('; ') : '']);
|
||||
rows.push([]);
|
||||
}
|
||||
|
||||
const strats = magic.strategies;
|
||||
if (Array.isArray(strats)) {
|
||||
rows.push(["STRATEGIES"]);
|
||||
strats.forEach((s: any) => {
|
||||
rows.push(["Direction", String(s.direction || '')]);
|
||||
rows.push(["Target", String(s.targetAudience || '')]);
|
||||
rows.push(["ROI", String(s.expectedROI || '')]);
|
||||
rows.push(["Risk", String(s.riskLevel || '')]);
|
||||
rows.push(["Timeframe", String(s.timeToResults || '')]);
|
||||
rows.push([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return rows.map(r => r.map(c => `"${String(c || '').replace(/"/g, '""')}"`).join(",")).join("\n");
|
||||
};
|
||||
|
||||
export const generateGoogleAdsExcel = (googleAds?: any, magic?: any): Blob => {
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// 1. Overview Sheet
|
||||
const overviewData: any[] = [
|
||||
["Attribute", "Value"],
|
||||
["Report Title", "Google Ads Strategy Report"],
|
||||
["Generated At", new Date().toLocaleString()],
|
||||
["Website URL", googleAds?.websiteUrl || magic?.websiteUrl || 'N/A'],
|
||||
[],
|
||||
["Performance Forecasts"],
|
||||
["Est. Impressions", googleAds?.predictions?.estimatedImpressions || 'N/A'],
|
||||
["Est. Clicks", googleAds?.predictions?.estimatedClicks || 'N/A'],
|
||||
["Est. CTR", googleAds?.predictions?.estimatedCtr || 'N/A'],
|
||||
["Est. Conversions", googleAds?.predictions?.estimatedConversions || 'N/A'],
|
||||
["Est. Conversion Rate", googleAds?.predictions?.conversionRate || 'N/A'],
|
||||
["Avg. CPC", googleAds?.predictions?.avgCpc || 'N/A'],
|
||||
[],
|
||||
["Historical Benchmarks"],
|
||||
["Avg. Industry CTR", googleAds?.historicalBenchmarks?.industryAverageCtr || 'N/A'],
|
||||
["Avg. Industry CPC", googleAds?.historicalBenchmarks?.industryAverageCpc || 'N/A'],
|
||||
["Seasonal Trends", googleAds?.historicalBenchmarks?.seasonalTrends || 'N/A'],
|
||||
["Geographic Insights", googleAds?.historicalBenchmarks?.geographicInsights || 'N/A']
|
||||
];
|
||||
const overviewSheet = XLSX.utils.aoa_to_sheet(overviewData);
|
||||
XLSX.utils.book_append_sheet(wb, overviewSheet, "Overview");
|
||||
|
||||
// 2. Keywords Sheet
|
||||
if (googleAds?.keywords) {
|
||||
const kw = googleAds.keywords;
|
||||
const kwData: any[] = [
|
||||
["Type", "Keyword", "Avg. CPC", "Monthly Volume", "Competition", "Difficulty"]
|
||||
];
|
||||
if (Array.isArray(kw.primary)) kw.primary.forEach((k: any) => kwData.push(["Primary", k.keyword, k.cpc, k.searchVolume, k.competition, k.difficultyScore]));
|
||||
if (Array.isArray(kw.longTail)) kw.longTail.forEach((k: any) => kwData.push(["Long-tail", k.keyword, k.cpc, k.searchVolume, k.competition, k.difficultyScore]));
|
||||
if (Array.isArray(kw.negative)) kw.negative.forEach((k: any) => kwData.push(["Negative", k.keyword, "", "", k.competition, ""]));
|
||||
|
||||
const kwSheet = XLSX.utils.aoa_to_sheet(kwData);
|
||||
XLSX.utils.book_append_sheet(wb, kwSheet, "Keywords");
|
||||
}
|
||||
|
||||
// 3. Ad Copies Sheet
|
||||
if (Array.isArray(googleAds?.adCopies)) {
|
||||
const adData: any[] = [
|
||||
["ID", "Variation", "Headlines", "Descriptions", "CTA", "Mobile Optimized", "Strategic Positioning"]
|
||||
];
|
||||
googleAds.adCopies.forEach((ad: any, i: number) => {
|
||||
adData.push([
|
||||
ad.id,
|
||||
`Variation ${i + 1}`,
|
||||
(ad.headlines || []).join(" | "),
|
||||
(ad.descriptions || []).join(" | "),
|
||||
ad.callToAction,
|
||||
ad.mobileOptimized ? "Yes" : "No",
|
||||
ad.positioning || "N/A"
|
||||
]);
|
||||
});
|
||||
const adSheet = XLSX.utils.aoa_to_sheet(adData);
|
||||
XLSX.utils.book_append_sheet(wb, adSheet, "Ad Copies");
|
||||
}
|
||||
|
||||
// 4. Competitors Sheet
|
||||
if (magic?.competitorInsights || magic?.marketAnalysis) {
|
||||
const compData: any[] = [
|
||||
["Competitor", "Website", "Est. Monthly Spend", "Target Audience", "Strengths", "Weaknesses", "Ad Strategy", "Top Keywords"]
|
||||
];
|
||||
if (Array.isArray(magic.competitorInsights)) {
|
||||
magic.competitorInsights.forEach((c: any) => {
|
||||
compData.push([
|
||||
c.competitor,
|
||||
c.website || 'N/A',
|
||||
c.estimatedSpend || 'N/A',
|
||||
c.targetAudience || 'N/A',
|
||||
(c.strengths || []).join(", "),
|
||||
(c.weaknesses || []).join(", "),
|
||||
c.adStrategy,
|
||||
(c.topKeywords || []).join(", ")
|
||||
]);
|
||||
});
|
||||
}
|
||||
const compSheet = XLSX.utils.aoa_to_sheet(compData);
|
||||
XLSX.utils.book_append_sheet(wb, compSheet, "Competitor Analysis");
|
||||
}
|
||||
|
||||
// 5. Strategies Sheet
|
||||
if (Array.isArray(magic?.strategies)) {
|
||||
const stratData: any[] = [
|
||||
["ID", "Strategic Direction", "Rationale", "Target Audience", "Competitive Advantage", "Expected ROI", "Risk Level", "Time to Results", "Success Metrics"]
|
||||
];
|
||||
magic.strategies.forEach((s: any) => {
|
||||
stratData.push([
|
||||
s.id,
|
||||
s.direction,
|
||||
s.rationale,
|
||||
s.targetAudience,
|
||||
s.competitiveAdvantage,
|
||||
s.expectedROI,
|
||||
s.riskLevel,
|
||||
s.timeToResults,
|
||||
(s.successMetrics || []).join(", ")
|
||||
]);
|
||||
});
|
||||
const stratSheet = XLSX.utils.aoa_to_sheet(stratData);
|
||||
XLSX.utils.book_append_sheet(wb, stratSheet, "Strategies");
|
||||
}
|
||||
|
||||
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
|
||||
return new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
};
|
||||
|
||||
export const generateGoogleAdsHTML = (googleAds?: any, magic?: any): string => {
|
||||
const data = JSON.stringify({ googleAds, magic }, null, 2);
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>360° Google Ads Strategy Intelligence Report</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Outfit', sans-serif; background-color: #020617; color: #f8fafc; }
|
||||
.glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); }
|
||||
.accent-gradient { background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); }
|
||||
.text-gradient { background: linear-gradient(to right, #818cf8, #c084fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-4 md:p-8 lg:p-12">
|
||||
<div class="max-w-7xl mx-auto space-y-12">
|
||||
<!-- Header -->
|
||||
<header class="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 pb-8 border-b border-slate-800">
|
||||
<div>
|
||||
<h1 class="text-4xl md:text-6xl font-extrabold text-gradient tracking-tight">Strategy Intelligence</h1>
|
||||
<p class="text-slate-400 mt-2 text-lg">360° Campaign Architecture for ${googleAds?.websiteUrl || 'New Project'}</p>
|
||||
</div>
|
||||
<div class="glass p-4 rounded-3xl flex items-center gap-4">
|
||||
<div class="text-right">
|
||||
<p class="text-xs text-slate-500 uppercase font-extrabold tracking-widest">Generated On</p>
|
||||
<p class="font-semibold">${new Date().toLocaleDateString()}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 accent-gradient rounded-2xl flex items-center justify-center font-bold text-xl">PA</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- KPI Grid -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="glass p-6 rounded-[2rem] border-indigo-500/20">
|
||||
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Est. Monthly Impressions</p>
|
||||
<p class="text-3xl font-extrabold mt-2 text-indigo-400">${googleAds?.predictions?.estimatedImpressions || '15k-25k'}</p>
|
||||
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-indigo-500 w-2/3"></div></div>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-[2rem] border-purple-500/20">
|
||||
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Target CTR</p>
|
||||
<p class="text-3xl font-extrabold mt-2 text-purple-400">${googleAds?.predictions?.estimatedCtr || '3.5%'}</p>
|
||||
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-purple-500 w-1/2"></div></div>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-[2rem] border-emerald-500/20">
|
||||
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Est. Conversions</p>
|
||||
<p class="text-3xl font-extrabold mt-2 text-emerald-400">${googleAds?.predictions?.estimatedConversions || '30-50'}</p>
|
||||
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-emerald-500 w-1/3"></div></div>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-[2rem] border-amber-500/20">
|
||||
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Industry Avg CTR</p>
|
||||
<p class="text-3xl font-extrabold mt-2 text-amber-400">${googleAds?.historicalBenchmarks?.industryAverageCtr || '3.1%'}</p>
|
||||
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-amber-500 w-4/5"></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Analytics Visuals -->
|
||||
<section class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2 glass p-8 rounded-[2.5rem]">
|
||||
<h2 class="text-2xl font-extrabold mb-6">Performance Forecast</h2>
|
||||
<div class="h-[350px]">
|
||||
<canvas id="performanceChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-8 rounded-[2.5rem]">
|
||||
<h2 class="text-2xl font-extrabold mb-6">Device Distribution</h2>
|
||||
<div class="h-[300px]">
|
||||
<canvas id="deviceChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-8 space-y-4">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-indigo-500"></div> Mobile</span>
|
||||
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.mobile || '60%'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-purple-500"></div> Desktop</span>
|
||||
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.desktop || '30%'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-slate-500"></div> Tablet</span>
|
||||
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.tablet || '10%'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Keyword Intelligence -->
|
||||
<section class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-3xl font-extrabold tracking-tight">Keyword Intelligence</h2>
|
||||
<span class="glass px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest text-indigo-400">Semantic Mapping Enabled</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Primary -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-indigo-500"></div> Primary Keywords
|
||||
</h3>
|
||||
${(googleAds?.keywords?.primary || []).slice(0, 10).map((k: any) => `
|
||||
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-indigo-500/50 transition-all cursor-default">
|
||||
<span class="font-semibold">${k.keyword}</span>
|
||||
<span class="text-xs text-indigo-400 font-bold">${k.cpc || '$1.50'}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<!-- Long-Tail -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-purple-500"></div> Long-Tail Intent
|
||||
</h3>
|
||||
${(googleAds?.keywords?.longTail || []).slice(0, 10).map((k: any) => `
|
||||
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-purple-500/50 transition-all cursor-default">
|
||||
<span class="font-semibold text-sm">${k.keyword}</span>
|
||||
<span class="text-[10px] text-slate-500 font-bold">${k.competition || 'low'}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<!-- Negative -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-rose-500"></div> Exclusion List
|
||||
</h3>
|
||||
${(googleAds?.keywords?.negative || []).slice(0, 8).map((k: any) => `
|
||||
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-rose-500/50 transition-all cursor-default grayscale opacity-50">
|
||||
<span class="font-semibold text-xs line-through text-slate-400">${k.keyword}</span>
|
||||
<span class="text-[10px] text-rose-500/50 font-bold">EXCLUDE</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Ad Copies -->
|
||||
<section class="space-y-8">
|
||||
<h2 class="text-3xl font-extrabold tracking-tight">High-Performance Ad Copy Suite</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
${(googleAds?.adCopies || []).map((ad: any, i: number) => `
|
||||
<div class="glass rounded-[2rem] overflow-hidden flex flex-col">
|
||||
<div class="p-6 border-b border-slate-800 flex justify-between items-center">
|
||||
<span class="text-xs font-bold uppercase tracking-widest text-slate-500">Variation ${i + 1}</span>
|
||||
<span class="bg-indigo-500/20 text-indigo-400 px-2.5 py-1 rounded-full text-[10px] font-black">${(ad.campaignType || 'Search').toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="p-8 space-y-6 flex-grow">
|
||||
${(ad.headlines || []).map((h: any) => `
|
||||
<div class="text-xl font-bold text-slate-100 leading-tight">${h}</div>
|
||||
`).join('')}
|
||||
<div class="h-px w-full bg-slate-800"></div>
|
||||
${(ad.descriptions || []).map((d: any) => `
|
||||
<div class="text-slate-400 text-sm leading-relaxed italic border-l-2 border-slate-700 pl-4">${d}</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="p-6 bg-slate-900/50 border-t border-slate-800 flex justify-between items-center">
|
||||
<div class="text-xs font-bold text-indigo-400">${ad.callToAction}</div>
|
||||
<div class="flex gap-1">
|
||||
<div class="w-1 h-3 rounded-full ${ad.mobileOptimized ? 'bg-emerald-500' : 'bg-slate-700'}"></div>
|
||||
<div class="w-3 h-3 rounded-full ${ad.mobileOptimized ? 'bg-emerald-500' : 'bg-slate-700'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Competitive Analysis -->
|
||||
${magic?.competitorInsights ? `
|
||||
<section class="space-y-8">
|
||||
<div class="flex justify-between items-end">
|
||||
<h2 class="text-3xl font-extrabold tracking-tight">Competitive Intelligence Matrix</h2>
|
||||
<div class="text-right">
|
||||
<p class="text-[10px] text-slate-500 uppercase font-black tracking-[0.2em] mb-1">Market Sentiment</p>
|
||||
<div class="flex gap-1.5 justify-end">
|
||||
<div class="w-4 h-2 rounded-full bg-emerald-500"></div>
|
||||
<div class="w-4 h-2 rounded-full bg-emerald-500"></div>
|
||||
<div class="w-4 h-2 rounded-full bg-emerald-400"></div>
|
||||
<div class="w-4 h-2 rounded-full bg-slate-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
${(magic.competitorInsights || []).map((c: any) => `
|
||||
<div class="glass p-8 rounded-[2.5rem] space-y-8">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="text-2xl font-black">${c.competitor}</h3>
|
||||
<p class="text-indigo-400 text-xs font-bold mt-1">${c.website || 'Direct Competitor'}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Est. Spend</p>
|
||||
<p class="text-sm font-black text-rose-400">${c.estimatedSpend || 'Undisclosed'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-3">
|
||||
<p class="text-[10px] text-emerald-400 uppercase font-black tracking-widest">Core Strengths</p>
|
||||
<ul class="text-sm space-y-2 text-slate-300">
|
||||
${(c.strengths || []).map((s: any) => `<li class="flex items-center gap-2"><div class="w-1.5 h-1.5 rounded-full bg-emerald-500"></div> ${s}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-[10px] text-rose-400 uppercase font-black tracking-widest">Weaknesses</p>
|
||||
<ul class="text-sm space-y-2 text-slate-400">
|
||||
${(c.weaknesses || []).map((w: any) => `<li class="flex items-center gap-2"><div class="w-1.5 h-1.5 rounded-full bg-rose-500"></div> ${w}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-indigo-500/5 p-6 rounded-2xl border border-indigo-500/10">
|
||||
<p class="text-[10px] text-indigo-400 uppercase font-bold tracking-widest mb-3">Counter-Strategy Rationale</p>
|
||||
<p class="text-sm text-slate-300 italic leading-relaxed">"${c.adStrategy}"</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</section>
|
||||
` : ''}
|
||||
|
||||
<!-- Strategies -->
|
||||
${magic?.strategies ? `
|
||||
<section class="space-y-8">
|
||||
<h2 class="text-3xl font-extrabold tracking-tight">Strategic Directions</h2>
|
||||
<div class="space-y-8">
|
||||
${(magic.strategies || []).map((s: any, idx: number) => `
|
||||
<div class="glass overflow-hidden rounded-[3rem] relative">
|
||||
<div class="absolute top-0 right-0 w-64 h-64 accent-gradient blur-[120px] opacity-10"></div>
|
||||
<div class="p-10 relative z-10 grid grid-cols-1 lg:grid-cols-4 gap-12">
|
||||
<!-- Left: Header -->
|
||||
<div class="lg:col-span-1 space-y-6">
|
||||
<div class="w-16 h-16 rounded-3xl accent-gradient flex items-center justify-center font-black text-2xl">${idx + 1}</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-extrabold leading-tight">${s.direction}</h3>
|
||||
<span class="inline-block mt-4 glass px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${s.riskLevel === 'low' ? 'text-emerald-400' : 'text-amber-400'}">${s.riskLevel || 'low'} risk profile</span>
|
||||
</div>
|
||||
<div class="pt-6 space-y-4">
|
||||
<div>
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Expected ROI</p>
|
||||
<p class="text-xl font-black text-emerald-400">${s.expectedROI}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Time to Impact</p>
|
||||
<p class="text-xl font-black text-slate-100">${s.timeToResults}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle: Context -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<div>
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-4">Strategic Rationale</p>
|
||||
<p class="text-lg text-slate-200 leading-relaxed font-light">${s.rationale}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-8">
|
||||
<div class="bg-white/5 p-6 rounded-[2rem]">
|
||||
<p class="text-[10px] text-indigo-400 uppercase font-black tracking-widest mb-3">Target Audience</p>
|
||||
<p class="text-sm font-semibold">${s.targetAudience}</p>
|
||||
</div>
|
||||
<div class="bg-white/5 p-6 rounded-[2rem]">
|
||||
<p class="text-[10px] text-purple-400 uppercase font-black tracking-widest mb-3">Competitive Edge</p>
|
||||
<p class="text-sm font-semibold">${s.competitiveAdvantage}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Channels/Metrics -->
|
||||
<div class="lg:col-span-1 bg-slate-900/80 p-8 rounded-[2.5rem] space-y-8 border border-white/5">
|
||||
<div>
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-6">Channel Matrix</p>
|
||||
<div class="space-y-4">
|
||||
${Object.entries(s.estimatedBudgetAllocation || {}).map(([c, v]: [string, any]) => `
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="capitalize text-slate-300">${c}</span>
|
||||
<span class="font-bold text-slate-100">${v}%</span>
|
||||
</div>
|
||||
<div class="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
|
||||
<div class="h-full accent-gradient" style="width: ${v}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-6 border-t border-slate-800">
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-4">Success Thresholds</p>
|
||||
<div class="flex flex-wrap gap-2 text-[10px]">
|
||||
${(s.successMetrics || []).map((m: any) => `
|
||||
<span class="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-1 rounded-md font-bold uppercase">${m}</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</section>
|
||||
` : ''}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="pt-12 border-t border-slate-800 flex flex-col md:flex-row justify-between items-center gap-6 pb-12">
|
||||
<p class="text-slate-500 text-sm font-semibold">© ${new Date().getFullYear()} PromptArch Intelligence Unit • Confident Strategic Asset</p>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-xs font-bold">PA</div>
|
||||
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-xs font-bold italic">Q</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Performance Forecast Chart
|
||||
const ctxPerf = document.getElementById('performanceChart').getContext('2d');
|
||||
new Chart(ctxPerf, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5', 'Week 6', 'Week 12'],
|
||||
datasets: [{
|
||||
label: 'Predicted Growth (Aggressive)',
|
||||
data: [10, 25, 45, 80, 110, 160, 450],
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 6,
|
||||
pointBackgroundColor: '#fff'
|
||||
}, {
|
||||
label: 'Predicted Growth (Standard)',
|
||||
data: [5, 12, 28, 55, 75, 100, 280],
|
||||
borderColor: '#a855f7',
|
||||
backgroundColor: 'transparent',
|
||||
borderDash: [5, 5],
|
||||
tension: 0.4,
|
||||
pointRadius: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { labels: { color: '#94a3b8', font: { family: 'Outfit', weight: 'bold' } } } },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { color: '#475569' } },
|
||||
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#475569' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Device Chart
|
||||
const ctxDev = document.getElementById('deviceChart').getContext('2d');
|
||||
new Chart(ctxDev, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Mobile', 'Desktop', 'Tablet'],
|
||||
datasets: [{
|
||||
data: [60, 30, 10],
|
||||
backgroundColor: ['#6366f1', '#a855f7', '#475569'],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 12
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '80%',
|
||||
plugins: { legend: { display: false } }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
1402
lib/i18n/translations.ts
Normal file
1402
lib/i18n/translations.ts
Normal file
File diff suppressed because it is too large
Load Diff
73
lib/safeJsonFetch.ts
Normal file
73
lib/safeJsonFetch.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
export class NonJsonResponseError extends Error {
|
||||
status: number;
|
||||
contentType: string | null;
|
||||
bodyPreview: string;
|
||||
|
||||
constructor(args: { status: number; contentType: string | null; bodyPreview: string }) {
|
||||
super(`Expected JSON but received ${args.contentType ?? "unknown content-type"} (HTTP ${args.status})`);
|
||||
this.name = "NonJsonResponseError";
|
||||
this.status = args.status;
|
||||
this.contentType = args.contentType;
|
||||
this.bodyPreview = args.bodyPreview;
|
||||
}
|
||||
}
|
||||
|
||||
type SafeJsonFetchResult<T> =
|
||||
| { ok: true; data: T }
|
||||
| { ok: false; error: { message: string; status?: number; bodyPreview?: string } };
|
||||
|
||||
export async function safeJsonFetch<T>(
|
||||
url: string,
|
||||
init?: RequestInit
|
||||
): Promise<SafeJsonFetchResult<T>> {
|
||||
const res = await fetch(url, init);
|
||||
|
||||
const contentType = res.headers.get("content-type");
|
||||
const text = await res.text();
|
||||
|
||||
// HTTP error — return readable details (don't JSON.parse blindly)
|
||||
if (!res.ok) {
|
||||
// Try JSON first if it looks like JSON
|
||||
if (contentType?.includes("application/json")) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return { ok: false, error: { message: parsed?.error ?? "Request failed", status: res.status } };
|
||||
} catch {
|
||||
// fall through to generic
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: `Request failed (HTTP ${res.status})`,
|
||||
status: res.status,
|
||||
bodyPreview: text.slice(0, 300),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Success but not JSON => this is exactly the "Unexpected token <" case
|
||||
if (!contentType?.includes("application/json")) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: `Server returned non-JSON (content-type: ${contentType ?? "unknown"})`,
|
||||
status: res.status,
|
||||
bodyPreview: text.slice(0, 300),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return { ok: true, data: JSON.parse(text) as T };
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: "Server returned invalid JSON",
|
||||
status: res.status,
|
||||
bodyPreview: text.slice(0, 300),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
573
lib/seo-report.ts
Normal file
573
lib/seo-report.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import ModelAdapter from "./model-adapter";
|
||||
import { OpenRouterService } from "./openrouter";
|
||||
|
||||
const adapter = new ModelAdapter();
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
|
||||
import type { ModelProvider, APIResponse, ChatMessage, AIAssistMessage } from "@/types";
|
||||
import OllamaCloudService from "./ollama-cloud";
|
||||
import ZaiPlanService from "./zai-plan";
|
||||
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
|
||||
import { OpenRouterService } from "./openrouter";
|
||||
|
||||
export interface ModelAdapterConfig {
|
||||
qwen?: QwenOAuthConfig;
|
||||
@@ -14,17 +15,22 @@ export interface ModelAdapterConfig {
|
||||
generalEndpoint?: string;
|
||||
codingEndpoint?: string;
|
||||
};
|
||||
openrouter?: {
|
||||
apiKey?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ModelAdapter {
|
||||
private ollamaService: OllamaCloudService;
|
||||
private zaiService: ZaiPlanService;
|
||||
private qwenService = qwenOAuthService;
|
||||
private openRouterService: OpenRouterService;
|
||||
private preferredProvider: ModelProvider;
|
||||
|
||||
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "ollama") {
|
||||
this.ollamaService = new OllamaCloudService(config.ollama);
|
||||
this.zaiService = new ZaiPlanService(config.zai);
|
||||
this.openRouterService = new OpenRouterService(config.openrouter);
|
||||
this.preferredProvider = preferredProvider;
|
||||
|
||||
if (config.qwen) {
|
||||
@@ -62,6 +68,10 @@ export class ModelAdapter {
|
||||
this.qwenService.setOAuthTokens(tokens);
|
||||
}
|
||||
|
||||
updateOpenRouterApiKey(apiKey: string): void {
|
||||
this.openRouterService = new OpenRouterService({ apiKey });
|
||||
}
|
||||
|
||||
async startQwenOAuth(): Promise<QwenOAuthToken> {
|
||||
return await this.qwenService.signIn();
|
||||
}
|
||||
@@ -74,6 +84,14 @@ export class ModelAdapter {
|
||||
return this.qwenService.hasOAuthToken();
|
||||
}
|
||||
|
||||
async validateConnection(provider: ModelProvider): Promise<APIResponse<{ valid: boolean; models?: string[] }>> {
|
||||
const service = this.getService(provider);
|
||||
if (!service || !service.validateConnection) {
|
||||
return { success: false, error: `Provider ${provider} does not support connection validation` };
|
||||
}
|
||||
return await service.validateConnection();
|
||||
}
|
||||
|
||||
private isProviderAuthenticated(provider: ModelProvider): boolean {
|
||||
switch (provider) {
|
||||
case "qwen":
|
||||
@@ -82,6 +100,8 @@ export class ModelAdapter {
|
||||
return this.ollamaService.hasAuth();
|
||||
case "zai":
|
||||
return this.zaiService.hasAuth();
|
||||
case "openrouter":
|
||||
return this.openRouterService.hasAuth();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -98,6 +118,21 @@ export class ModelAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
private getService(provider: ModelProvider): any {
|
||||
switch (provider) {
|
||||
case "qwen":
|
||||
return this.qwenService;
|
||||
case "ollama":
|
||||
return this.ollamaService;
|
||||
case "zai":
|
||||
return this.zaiService;
|
||||
case "openrouter":
|
||||
return this.openRouterService;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async callWithFallback<T>(
|
||||
operation: (service: any) => Promise<APIResponse<T>>,
|
||||
providers: ModelProvider[]
|
||||
@@ -132,6 +167,9 @@ export class ModelAdapter {
|
||||
case "zai":
|
||||
service = this.zaiService;
|
||||
break;
|
||||
case "openrouter":
|
||||
service = this.openRouterService;
|
||||
break;
|
||||
}
|
||||
|
||||
const result = await operation(service);
|
||||
@@ -162,30 +200,158 @@ export class ModelAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
async enhancePrompt(prompt: string, provider?: ModelProvider, model?: string, options?: { toolCategory?: string; template?: string; diagnostics?: string }): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.enhancePrompt(prompt, model), providers);
|
||||
return this.callWithFallback((service) => service.enhancePrompt(prompt, model, options), providers);
|
||||
}
|
||||
|
||||
async generatePRD(idea: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generatePRD(idea, model), providers);
|
||||
}
|
||||
|
||||
async generateActionPlan(prd: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateActionPlan(prd, model), providers);
|
||||
}
|
||||
|
||||
async generateUXDesignerPrompt(appDescription: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers);
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
animationStyle?: string;
|
||||
audienceStyle?: string;
|
||||
themeColors?: string[];
|
||||
brandColors?: string[];
|
||||
} = {},
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateSlides(topic, options, model), providers);
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
specialInstructions?: string;
|
||||
} = { productsServices: [] },
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateGoogleAds(websiteUrl, options, model), providers);
|
||||
}
|
||||
|
||||
async generateMagicWand(
|
||||
websiteUrl: string,
|
||||
product: string,
|
||||
budget: number,
|
||||
specialInstructions?: string,
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateMagicWand(websiteUrl, product, budget, specialInstructions, model), providers);
|
||||
}
|
||||
|
||||
async generateMarketResearch(
|
||||
options: {
|
||||
websiteUrl: string;
|
||||
additionalUrls?: string[];
|
||||
competitors: string[];
|
||||
productMapping: string;
|
||||
specialInstructions?: string;
|
||||
},
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateMarketResearch(options, model), providers);
|
||||
}
|
||||
|
||||
async generateAIAssist(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
},
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateAIAssist(options, model), providers);
|
||||
}
|
||||
|
||||
async generateAIAssistStream(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
onChunk: (chunk: string) => void;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<void>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai", "openrouter");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
|
||||
let lastError: string | null = null;
|
||||
|
||||
for (const candidate of providers) {
|
||||
const service = this.getService(candidate);
|
||||
if (!service?.generateAIAssistStream) {
|
||||
continue;
|
||||
}
|
||||
if (!this.isProviderAuthenticated(candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await service.generateAIAssistStream(options, model);
|
||||
if (response.success) {
|
||||
return response;
|
||||
}
|
||||
if (response.error) {
|
||||
lastError = response.error;
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
lastError = errorMessage || lastError;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: lastError || "No authenticated providers available for streaming",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string,
|
||||
@@ -204,6 +370,9 @@ export class ModelAdapter {
|
||||
case "zai":
|
||||
service = this.zaiService;
|
||||
break;
|
||||
case "openrouter":
|
||||
service = this.openRouterService;
|
||||
break;
|
||||
}
|
||||
|
||||
return await service.chatCompletion(messages, model);
|
||||
@@ -220,9 +389,10 @@ export class ModelAdapter {
|
||||
qwen: this.qwenService.getAvailableModels(),
|
||||
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
|
||||
zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||
openrouter: ["anthropic/claude-3.5-sonnet", "google/gemini-2.0-flash-exp:free", "meta-llama/llama-3.3-70b-instruct", "openai/gpt-4o-mini", "deepseek/deepseek-chat-v3-0324", "qwen/qwen-2.5-72b-instruct"],
|
||||
};
|
||||
const models: Record<ModelProvider, string[]> = { ...fallbackModels };
|
||||
|
||||
|
||||
if (provider === "ollama" || !provider) {
|
||||
try {
|
||||
const ollamaModels = await this.ollamaService.listModels();
|
||||
@@ -255,6 +425,8 @@ export class ModelAdapter {
|
||||
return this.ollamaService.getAvailableModels();
|
||||
case "zai":
|
||||
return this.zaiService.getAvailableModels();
|
||||
case "openrouter":
|
||||
return this.openRouterService.getAvailableModels();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChatMessage, APIResponse } from "@/types";
|
||||
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||
|
||||
export interface OllamaCloudConfig {
|
||||
apiKey?: string;
|
||||
@@ -164,27 +164,82 @@ export class OllamaCloudService {
|
||||
return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS;
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
|
||||
async enhancePrompt(prompt: string, model?: string, options?: { toolCategory?: string; template?: string; diagnostics?: string }): Promise<APIResponse<string>> {
|
||||
const toolCategory = options?.toolCategory || 'reasoning';
|
||||
const template = options?.template || 'rtf';
|
||||
const diagnostics = options?.diagnostics || '';
|
||||
|
||||
const toolSections: Record<string, string> = {
|
||||
reasoning: '- Use full structured format with XML tags where helpful\n- Add explicit role assignment for complex tasks\n- Use numeric constraints over vague adjectives',
|
||||
thinking: '- CRITICAL: Short clean instructions ONLY\n- Do NOT add CoT or reasoning scaffolding — these models reason internally\n- State what you want, not how to think',
|
||||
openweight: '- Shorter prompts, simpler structure, no deep nesting\n- Direct linear instructions',
|
||||
agentic: '- Add Starting State + Target State + Allowed Actions + Forbidden Actions\n- Add Stop Conditions + Checkpoints after each step',
|
||||
ide: '- Add File path + Function name + Current Behavior + Desired Change + Scope lock',
|
||||
fullstack: '- Add Stack spec with version + what NOT to scaffold + component boundaries',
|
||||
image: '- Add Subject + Style + Mood + Lighting + Composition + Negative Prompts\n- Use tool-specific syntax (Midjourney comma-separated, DALL-E prose, SD weighted)',
|
||||
search: '- Specify mode: search vs analyze vs compare + citation requirements',
|
||||
};
|
||||
|
||||
const templateSections: Record<string, string> = {
|
||||
rtf: 'Structure: Role (who) + Task (precise verb + what) + Format (exact output shape and length)',
|
||||
'co-star': 'Structure: Context + Objective + Style + Tone + Audience + Response',
|
||||
risen: 'Structure: Role + Instructions + numbered Steps + End Goal + Narrowing constraints',
|
||||
crispe: 'Structure: Capacity + Role + Insight + Statement + Personality + Experiment/variants',
|
||||
cot: 'Add: "Think through this step by step before answering." Only for standard reasoning models, NOT for o1/o3/R1.',
|
||||
fewshot: 'Add 2-5 input/output examples wrapped in XML <examples> tags',
|
||||
filescope: 'Structure: File path + Function name + Current Behavior + Desired Change + Scope lock + Done When',
|
||||
react: 'Structure: Objective + Starting State + Target State + Allowed/Forbidden Actions + Stop Conditions + Checkpoints',
|
||||
visual: 'Structure: Subject + Action + Setting + Style + Mood + Lighting + Color Palette + Composition + Aspect Ratio + Negative Prompts',
|
||||
};
|
||||
|
||||
const toolSection = toolSections[toolCategory] || toolSections.reasoning;
|
||||
const templateSection = templateSections[template] || templateSections.rtf;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an expert prompt engineer. Your task is to enhance user prompts to make them more precise, actionable, and effective for AI coding agents.
|
||||
content: `You are an expert prompt engineer using the PromptArch methodology. Enhance the user\'s prompt to be production-ready.
|
||||
|
||||
Apply these principles:
|
||||
1. Add specific context about project and requirements
|
||||
2. Clarify constraints and preferences
|
||||
3. Define expected output format clearly
|
||||
4. Include edge cases and error handling requirements
|
||||
5. Specify testing and validation criteria
|
||||
STEP 1 — DIAGNOSE AND FIX these failure patterns:
|
||||
- Vague task verb -> replace with precise operation
|
||||
- Two tasks in one -> keep primary task, note the split
|
||||
- No success criteria -> add "Done when: [specific measurable condition]"
|
||||
- Missing output format -> add explicit format lock (structure, length, type)
|
||||
- No role assignment (complex tasks) -> add domain-specific expert identity
|
||||
- Vague aesthetic ("professional", "clean") -> concrete measurable specs
|
||||
- No scope boundary -> add explicit scope lock
|
||||
- Over-permissive language -> add constraints and boundaries
|
||||
- Emotional description -> extract specific technical fault
|
||||
- Implicit references -> restate fully
|
||||
- No grounding for factual tasks -> add certainty constraint
|
||||
- No CoT for logic tasks -> add step-by-step reasoning
|
||||
|
||||
Return ONLY the enhanced prompt, no explanations or extra text.`,
|
||||
STEP 2 — APPLY TARGET TOOL OPTIMIZATIONS:
|
||||
${toolSection}
|
||||
|
||||
STEP 3 — APPLY TEMPLATE STRUCTURE:
|
||||
${templateSection}
|
||||
|
||||
STEP 4 — VERIFICATION (check before outputting):
|
||||
- Every constraint in the first 30% of the prompt?
|
||||
- MUST/NEVER over should/avoid?
|
||||
- Every sentence load-bearing with zero padding?
|
||||
- Format explicit with stated length?
|
||||
- Scope bounded?
|
||||
- Would this produce correct output on first try?
|
||||
|
||||
STEP 5 — OUTPUT:
|
||||
Output ONLY the enhanced prompt. No explanations, no commentary, no markdown code fences.
|
||||
The prompt must be ready to paste directly into the target AI tool.${diagnostics ? '\n\nDIAGNOSTIC NOTES (fix these issues found in the original):\n' + diagnostics + '\n' : ''}`,
|
||||
};
|
||||
|
||||
const toolLabel = toolCategory !== 'reasoning' ? ` for ${toolCategory} AI tool` : '';
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
|
||||
content: `Enhance this prompt${toolLabel}:\n\n${prompt}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "${default_model}");
|
||||
}
|
||||
|
||||
async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> {
|
||||
@@ -303,6 +358,624 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
animationStyle?: string;
|
||||
audienceStyle?: string;
|
||||
themeColors?: string[];
|
||||
brandColors?: string[];
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
language = "English",
|
||||
theme = "executive-dark",
|
||||
slideCount = 10,
|
||||
audience = "Executives & C-Suite",
|
||||
organization = "",
|
||||
animationStyle = "Professional",
|
||||
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||
brandColors = []
|
||||
} = options;
|
||||
|
||||
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||
const brandColorStr = brandColors.length > 0
|
||||
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||
: "";
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||
|
||||
Your slides must be VISUALLY SPECTACULAR with:
|
||||
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||
- Sophisticated gradient backgrounds with depth
|
||||
- SVG charts and data visualizations inline
|
||||
- Glassmorphism and neumorphism effects
|
||||
- Professional typography with Inter/SF Pro fonts
|
||||
- Strategic use of whitespace
|
||||
- Micro-animations on hover/focus states
|
||||
- Progress indicators and visual hierarchy
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Compelling Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Plain text content summary",
|
||||
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN SYSTEM:
|
||||
- Primary: ${brandColors[0] || primaryColor}
|
||||
- Secondary: ${brandColors[1] || secondaryColor}
|
||||
- Background: ${bgColor}
|
||||
- Text: ${textColor}${brandColorStr}
|
||||
|
||||
ANIMATION STYLE: ${animationStyle}
|
||||
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||
|
||||
CSS ANIMATIONS TO INCLUDE:
|
||||
\`\`\`css
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||
\`\`\`
|
||||
|
||||
SLIDE TYPES TO CREATE:
|
||||
1. TITLE SLIDE: Hero-style with animated gradient background, large typography
|
||||
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||
4. KEY METRICS: Large animated numbers with KPI cards
|
||||
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||
7. QUOTE: Large typography with decorative quote marks
|
||||
8. CALL-TO-ACTION: Bold CTA with pulsing button effect
|
||||
|
||||
TARGET AUDIENCE: ${audience}
|
||||
AUDIENCE STYLE: ${audienceStyle}
|
||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||
|
||||
REQUIREMENTS:
|
||||
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
|
||||
- ALL content in ${language}
|
||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||
- Use animation-delay for staggered reveal effects
|
||||
- Include decorative background elements (gradients, shapes)
|
||||
- Ensure text contrast meets WCAG AA standards
|
||||
- Add subtle shadow/glow effects for depth`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a STUNNING, ANIMATED presentation about:
|
||||
|
||||
${topic}
|
||||
|
||||
SPECIFICATIONS:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Slides: ${slideCount}
|
||||
- Audience: ${audience} (${audienceStyle})
|
||||
- Animation Style: ${animationStyle}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||
|
||||
Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients, and corporate-ready design!`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
specialInstructions?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English",
|
||||
specialInstructions = ""
|
||||
} = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an EXPERT Google Ads strategist. Create HIGH-CONVERTING campaigns with comprehensive keyword research, compelling ad copy, and optimized campaign structures.
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"keywords": {
|
||||
"primary": [{"keyword": "term", "type": "primary", "searchVolume": 12000, "competition": "medium", "cpc": "$2.50"}],
|
||||
"longTail": [{"keyword": "specific term", "type": "long-tail", "searchVolume": 1200, "competition": "low", "cpc": "$1.25"}],
|
||||
"negative": [{"keyword": "exclude term", "type": "negative", "competition": "low"}]
|
||||
},
|
||||
"adCopies": [{
|
||||
"id": "ad-1",
|
||||
"campaignType": "search",
|
||||
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||
"callToAction": "Get Started",
|
||||
"mobileOptimized": true,
|
||||
"positioning": "Value proposition used"
|
||||
}],
|
||||
"campaigns": [{
|
||||
"id": "campaign-1",
|
||||
"name": "Campaign Name",
|
||||
"type": "search",
|
||||
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||
"biddingStrategy": "Maximize conversions",
|
||||
"targeting": {
|
||||
"locations": ["Specific regions/cities"],
|
||||
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
|
||||
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
|
||||
"schedule": ["Mon-Fri, 9am-5pm"]
|
||||
},
|
||||
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
|
||||
}],
|
||||
"implementation": {
|
||||
"setupSteps": ["Step 1", "Step 2"],
|
||||
"qualityScoreTips": ["Tip 1", "Tip 2"],
|
||||
"trackingSetup": ["Conversion tag info", "GTM setup"],
|
||||
"optimizationTips": ["Tip 1", "Tip 2"]
|
||||
},
|
||||
"predictions": {
|
||||
"estimatedClicks": "500-800",
|
||||
"estimatedImpressions": "15,000-25,000",
|
||||
"estimatedCtr": "3.5%",
|
||||
"estimatedConversions": "30-50",
|
||||
"conversionRate": "4.2%",
|
||||
"avgCpc": "$1.85"
|
||||
},
|
||||
"historicalBenchmarks": {
|
||||
"industryAverageCtr": "3.1%",
|
||||
"industryAverageCpc": "$2.10",
|
||||
"seasonalTrends": "Peak in Q4",
|
||||
"geographicInsights": "London/NY show highest ROI"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Requirements:
|
||||
- 10-15 primary keywords, 15-20 long-tail, 5-10 negative
|
||||
- Headlines max 30 chars, descriptions max 90 chars
|
||||
- 3-5 ad variations per campaign
|
||||
- Include budget and targeting recommendations`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a Google Ads campaign for:
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
PRODUCTS/SERVICES: ${productsServices.join(", ")}
|
||||
TARGET AUDIENCE: ${targetAudience}
|
||||
INDUSTRY: ${industry}
|
||||
LANGUAGE: ${language}
|
||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||
|
||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateMagicWand(
|
||||
websiteUrl: string,
|
||||
product: string,
|
||||
budget: number,
|
||||
specialInstructions?: string,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy.
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"marketAnalysis": {
|
||||
"industrySize": "Estimated market size",
|
||||
"growthRate": "Annual growth percentage",
|
||||
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||
},
|
||||
"competitorInsights": [
|
||||
{
|
||||
"competitor": "Competitor Name",
|
||||
"website": "URL",
|
||||
"estimatedSpend": "$10k-$50k/mo",
|
||||
"targetAudience": "Who they target",
|
||||
"strengths": ["Strength 1", "Strength 2"],
|
||||
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||
"adStrategy": "Their approach",
|
||||
"topKeywords": ["keyword 1", "keyword 2"],
|
||||
"adCopyExamples": [
|
||||
{"headline": "Example Headline", "description": "Example Description"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"strategies": [
|
||||
{
|
||||
"id": "strategy-1",
|
||||
"direction": "Strategic Direction Name",
|
||||
"rationale": "Why this strategy works",
|
||||
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
|
||||
"targetingDetails": {
|
||||
"geography": "Primary locations",
|
||||
"demographics": "Specific age/gender groups",
|
||||
"behavior": "User behaviors"
|
||||
},
|
||||
"competitiveAdvantage": "How this beats competitors",
|
||||
"messagingPillars": ["Pillar 1", "Pillar 2"],
|
||||
"keyMessages": ["Message 1", "Message 2"],
|
||||
"adCopyGuide": {
|
||||
"headlines": ["Headline 1", "Headline 2"],
|
||||
"descriptions": ["Description 1", "Description 2"],
|
||||
"keywords": ["keyword 1", "keyword 2"],
|
||||
"setupGuide": "Step-by-step for Google Ads Manager"
|
||||
},
|
||||
"recommendedChannels": ["Search", "Display", "YouTube"],
|
||||
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||
"expectedROI": "150-200%",
|
||||
"riskLevel": "low",
|
||||
"timeToResults": "2-3 months",
|
||||
"successMetrics": ["CTR > 3%", "CPA < $20"]
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
- Provide 5-7 DISTINCT strategic directions
|
||||
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||
- Include REAL competitive insights based on industry knowledge
|
||||
- Risk levels: "low", "medium", or "high"
|
||||
- AD COPY GUIDE must be incredibly "noob-friendly" - explain exactly where to paste each field in Google Ads Manager
|
||||
- Headlines MUST be under 30 characters
|
||||
- Descriptions MUST be under 90 characters`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
PRODUCT/SERVICE: ${product}
|
||||
MONTHLY BUDGET: $${budget}
|
||||
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||
|
||||
Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateMarketResearch(
|
||||
options: {
|
||||
websiteUrl: string;
|
||||
additionalUrls?: string[];
|
||||
competitors: string[];
|
||||
productMapping: string;
|
||||
specialInstructions?: string;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const systemPrompt = `You are a WORLD-CLASS Market Research Analyst and Competitive Intelligence Expert.
|
||||
Your objective is to perform a deep-dive analysis of a business and its competitors based on provided URLs and product mappings.
|
||||
|
||||
You MUST return your analysis in the following STRICT JSON format:
|
||||
{
|
||||
"executiveSummary": "A concise overview of the market landscape and key findings.",
|
||||
"priceComparisonMatrix": [
|
||||
{
|
||||
"product": "Product Name",
|
||||
"userPrice": "$XX.XX",
|
||||
"competitorPrices": [
|
||||
{ "competitor": "Competitor Name", "price": "$XX.XX", "url": "https://competitor.com/product-page" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"featureComparisonTable": [
|
||||
{
|
||||
"feature": "Feature Name",
|
||||
"userStatus": true/false/text,
|
||||
"competitorStatus": [
|
||||
{ "competitor": "Competitor Name", "status": true/false/text }
|
||||
]
|
||||
}
|
||||
],
|
||||
"marketPositioning": {
|
||||
"landscape": "Description of the current market state.",
|
||||
"segmentation": "Analysis of target customer segments."
|
||||
},
|
||||
"competitiveAnalysis": {
|
||||
"advantages": ["Point 1", "Point 2"],
|
||||
"disadvantages": ["Point 1", "Point 2"]
|
||||
},
|
||||
"recommendations": ["Actionable step 1", "Actionable step 2"],
|
||||
"methodology": "Brief description of the research process."
|
||||
}
|
||||
|
||||
Requirements:
|
||||
1. Base your analysis on realistic price and feature estimates if exact data isn't visible.
|
||||
2. Focus on core technical/business value rather than marketing fluff.
|
||||
3. Ensure JSON is valid and properly escaped.`;
|
||||
|
||||
const userMsg = `WEBSITE TO ANALYZE: ${options.websiteUrl}
|
||||
ADDITIONAL COMPANY URLS: ${options.additionalUrls?.join(', ') || 'None'}
|
||||
COMPETITOR URLS: ${options.competitors.join(', ')}
|
||||
PRODUCT/FEATURE MAPPING: ${options.productMapping}
|
||||
SPECIAL REQUESTS: ${options.specialInstructions || 'Perform comprehensive analysis'}
|
||||
|
||||
Provide a COMPREHENSIVE competitive intelligence report.`;
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userMsg }
|
||||
];
|
||||
|
||||
return await this.chatCompletion(messages, model || this.getAvailableModels()[0]);
|
||||
}
|
||||
|
||||
async generateAIAssist(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch.
|
||||
Your goal is to provide intelligent conversational support and switch to specialized agents when necessary.
|
||||
|
||||
CURRENT SPECIALIZED AGENTS:
|
||||
- content: Content creation and optimization expert.
|
||||
- seo: SEO analyst and recommendations specialist.
|
||||
- smm: SMM strategy and social content planner.
|
||||
- pm: Project planning and management lead.
|
||||
- code: Code architect (JavaScript/TypeScript/React focus).
|
||||
- design: UI/UX designer.
|
||||
- web: HTML/CSS/JS web development specialist with real-time preview.
|
||||
- app: Mobile-first app development specialist with real-time preview.
|
||||
|
||||
STRICT OUTPUT FORMAT:
|
||||
You MUST respond in JSON format if you want to activate a preview or switch agents.
|
||||
{
|
||||
"content": "Your natural language response here...",
|
||||
"agent": "agent_id_to_switch_to (optional)",
|
||||
"preview": { // (optional)
|
||||
"type": "code" | "design" | "content" | "seo",
|
||||
"data": "The actual code, layout, or content to preview",
|
||||
"language": "javascript/html/css/markdown (optional)"
|
||||
}
|
||||
}
|
||||
|
||||
ROUTING LOGIC:
|
||||
- If user asks for code, switch to 'code' or 'web'.
|
||||
- If user asks for design/mockups, switch to 'design'.
|
||||
- If user asks for market/SEO, switch to 'seo'.
|
||||
- If user asks for marketing/social, switch to 'smm'.
|
||||
- Maintain the 'content' of the conversation regardless of the agent switch.
|
||||
|
||||
PREVIEW GUIDELINES:
|
||||
- For 'web'/'app', provide full runnable HTML/CSS/JS.
|
||||
- For 'code', provide clean, commented snippets.
|
||||
- For 'design', provide text-based UI components or layout structures.
|
||||
|
||||
RESPONSE TIME REQUIREMENT: Be concise and accurate.`;
|
||||
|
||||
const chatMessages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...options.messages.map(m => ({
|
||||
role: m.role as "user" | "assistant" | "system",
|
||||
content: m.content
|
||||
}))
|
||||
];
|
||||
|
||||
return await this.chatCompletion(chatMessages, model || this.getAvailableModels()[0]);
|
||||
}
|
||||
|
||||
async generateAIAssistStream(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
onChunk: (chunk: string) => void;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<void>> {
|
||||
try {
|
||||
// ... existing prompt logic ...
|
||||
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
|
||||
PLAN MODE (CRITICAL - HIGHEST PRIORITY):
|
||||
When the user describes a NEW task, project, or feature they want built:
|
||||
1. DO NOT generate any code, [PREVIEW] tags, or implementation details.
|
||||
2. Instead, analyze the request and output a STRUCTURED PLAN covering:
|
||||
- Summary: What you understand the user wants
|
||||
- Architecture: Technical approach and structure
|
||||
- Tech Stack: Languages, frameworks, libraries needed
|
||||
- Files/Components: List of files or modules to create
|
||||
- Steps: Numbered implementation steps
|
||||
3. Format the plan in clean Markdown with headers and bullet points.
|
||||
4. Keep plans concise but thorough. Focus on the WHAT and HOW, not the actual code.
|
||||
5. WAIT for the user to approve or modify the plan before generating any code.
|
||||
|
||||
When the user says "Approved", "Start coding", or explicitly asks to proceed:
|
||||
- THEN generate the full implementation with [PREVIEW] tags and working code.
|
||||
- Follow the approved plan exactly.
|
||||
|
||||
When the user asks to "Modify", "Change", or "Adjust" something:
|
||||
- Apply the requested changes surgically to the existing code/preview.
|
||||
- Output updated [PREVIEW] with the full modified code.
|
||||
|
||||
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- 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.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
- design: UI/UX Designer. Create high-fidelity mockups and components.
|
||||
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
|
||||
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
|
||||
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
|
||||
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
|
||||
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
|
||||
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||
|
||||
BACKEND LOGIC & SIMULATION:
|
||||
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||
|
||||
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||
- DO NOT regenerate the entire design if it was not requested.
|
||||
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
|
||||
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
|
||||
|
||||
CANVAS MODE:
|
||||
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
|
||||
- Inside [PREVIEW], output ONLY the actual code or structured data.
|
||||
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
|
||||
|
||||
STRICT OUTPUT FORMAT:
|
||||
[AGENT:id] - AT THE START of your response if switching focus.
|
||||
[PREVIEW:type:language]
|
||||
ACTUAL_FUNCTIONAL_CODE_OR_DATA
|
||||
[/PREVIEW]
|
||||
|
||||
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
|
||||
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
|
||||
- The change log should appear in the CHAT, NOT inside the preview code.
|
||||
- Example format:
|
||||
[/PREVIEW]
|
||||
|
||||
**Change Log:**
|
||||
- Added feature X
|
||||
- Modified component Y
|
||||
- Fixed issue Z
|
||||
|
||||
IMPORTANT: IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...options.messages.map(m => ({
|
||||
role: m.role as "user" | "assistant" | "system",
|
||||
content: m.content
|
||||
}))
|
||||
];
|
||||
|
||||
const response = await fetch(LOCAL_CHAT_URL, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders({ "Content-Type": "application/json" }),
|
||||
signal: options.signal,
|
||||
body: JSON.stringify({
|
||||
model: model || this.getAvailableModels()[0],
|
||||
messages,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Stream request failed");
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error("No reader");
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
buffer += chunk;
|
||||
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
if (data.message?.content) {
|
||||
options.onChunk(data.message.content);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing stream line", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default OllamaCloudService;
|
||||
|
||||
1026
lib/services/openrouter.ts
Normal file
1026
lib/services/openrouter.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import type { ChatMessage, APIResponse } from "@/types";
|
||||
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||
|
||||
const DEFAULT_QWEN_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
const TOKEN_STORAGE_KEY = "promptarch-qwen-tokens";
|
||||
@@ -491,27 +491,82 @@ export class QwenOAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
|
||||
async enhancePrompt(prompt: string, model?: string, options?: { toolCategory?: string; template?: string; diagnostics?: string }): Promise<APIResponse<string>> {
|
||||
const toolCategory = options?.toolCategory || 'reasoning';
|
||||
const template = options?.template || 'rtf';
|
||||
const diagnostics = options?.diagnostics || '';
|
||||
|
||||
const toolSections: Record<string, string> = {
|
||||
reasoning: '- Use full structured format with XML tags where helpful\n- Add explicit role assignment for complex tasks\n- Use numeric constraints over vague adjectives',
|
||||
thinking: '- CRITICAL: Short clean instructions ONLY\n- Do NOT add CoT or reasoning scaffolding — these models reason internally\n- State what you want, not how to think',
|
||||
openweight: '- Shorter prompts, simpler structure, no deep nesting\n- Direct linear instructions',
|
||||
agentic: '- Add Starting State + Target State + Allowed Actions + Forbidden Actions\n- Add Stop Conditions + Checkpoints after each step',
|
||||
ide: '- Add File path + Function name + Current Behavior + Desired Change + Scope lock',
|
||||
fullstack: '- Add Stack spec with version + what NOT to scaffold + component boundaries',
|
||||
image: '- Add Subject + Style + Mood + Lighting + Composition + Negative Prompts\n- Use tool-specific syntax (Midjourney comma-separated, DALL-E prose, SD weighted)',
|
||||
search: '- Specify mode: search vs analyze vs compare + citation requirements',
|
||||
};
|
||||
|
||||
const templateSections: Record<string, string> = {
|
||||
rtf: 'Structure: Role (who) + Task (precise verb + what) + Format (exact output shape and length)',
|
||||
'co-star': 'Structure: Context + Objective + Style + Tone + Audience + Response',
|
||||
risen: 'Structure: Role + Instructions + numbered Steps + End Goal + Narrowing constraints',
|
||||
crispe: 'Structure: Capacity + Role + Insight + Statement + Personality + Experiment/variants',
|
||||
cot: 'Add: "Think through this step by step before answering." Only for standard reasoning models, NOT for o1/o3/R1.',
|
||||
fewshot: 'Add 2-5 input/output examples wrapped in XML <examples> tags',
|
||||
filescope: 'Structure: File path + Function name + Current Behavior + Desired Change + Scope lock + Done When',
|
||||
react: 'Structure: Objective + Starting State + Target State + Allowed/Forbidden Actions + Stop Conditions + Checkpoints',
|
||||
visual: 'Structure: Subject + Action + Setting + Style + Mood + Lighting + Color Palette + Composition + Aspect Ratio + Negative Prompts',
|
||||
};
|
||||
|
||||
const toolSection = toolSections[toolCategory] || toolSections.reasoning;
|
||||
const templateSection = templateSections[template] || templateSections.rtf;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an expert prompt engineer. Your task is to enhance user prompts to make them more precise, actionable, and effective for AI coding agents.
|
||||
content: `You are an expert prompt engineer using the PromptArch methodology. Enhance the user\'s prompt to be production-ready.
|
||||
|
||||
Apply these principles:
|
||||
1. Add specific context about project and requirements
|
||||
2. Clarify constraints and preferences
|
||||
3. Define expected output format clearly
|
||||
4. Include edge cases and error handling requirements
|
||||
5. Specify testing and validation criteria
|
||||
STEP 1 — DIAGNOSE AND FIX these failure patterns:
|
||||
- Vague task verb -> replace with precise operation
|
||||
- Two tasks in one -> keep primary task, note the split
|
||||
- No success criteria -> add "Done when: [specific measurable condition]"
|
||||
- Missing output format -> add explicit format lock (structure, length, type)
|
||||
- No role assignment (complex tasks) -> add domain-specific expert identity
|
||||
- Vague aesthetic ("professional", "clean") -> concrete measurable specs
|
||||
- No scope boundary -> add explicit scope lock
|
||||
- Over-permissive language -> add constraints and boundaries
|
||||
- Emotional description -> extract specific technical fault
|
||||
- Implicit references -> restate fully
|
||||
- No grounding for factual tasks -> add certainty constraint
|
||||
- No CoT for logic tasks -> add step-by-step reasoning
|
||||
|
||||
Return ONLY the enhanced prompt, no explanations or extra text.`,
|
||||
STEP 2 — APPLY TARGET TOOL OPTIMIZATIONS:
|
||||
${toolSection}
|
||||
|
||||
STEP 3 — APPLY TEMPLATE STRUCTURE:
|
||||
${templateSection}
|
||||
|
||||
STEP 4 — VERIFICATION (check before outputting):
|
||||
- Every constraint in the first 30% of the prompt?
|
||||
- MUST/NEVER over should/avoid?
|
||||
- Every sentence load-bearing with zero padding?
|
||||
- Format explicit with stated length?
|
||||
- Scope bounded?
|
||||
- Would this produce correct output on first try?
|
||||
|
||||
STEP 5 — OUTPUT:
|
||||
Output ONLY the enhanced prompt. No explanations, no commentary, no markdown code fences.
|
||||
The prompt must be ready to paste directly into the target AI tool.${diagnostics ? '\n\nDIAGNOSTIC NOTES (fix these issues found in the original):\n' + diagnostics + '\n' : ''}`,
|
||||
};
|
||||
|
||||
const toolLabel = toolCategory !== 'reasoning' ? ` for ${toolCategory} AI tool` : '';
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
|
||||
content: `Enhance this prompt${toolLabel}:\n\n${prompt}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "${default_model}");
|
||||
}
|
||||
|
||||
async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> {
|
||||
@@ -631,11 +686,600 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
const models = [
|
||||
"coder-model",
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
animationStyle?: string;
|
||||
audienceStyle?: string;
|
||||
themeColors?: string[];
|
||||
brandColors?: string[];
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
language = "English",
|
||||
theme = "executive-dark",
|
||||
slideCount = 10,
|
||||
audience = "Executives & C-Suite",
|
||||
organization = "",
|
||||
animationStyle = "Professional",
|
||||
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||
brandColors = []
|
||||
} = options;
|
||||
|
||||
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||
const brandColorStr = brandColors.length > 0
|
||||
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||
: "";
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||
|
||||
Your slides must be VISUALLY SPECTACULAR with:
|
||||
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||
- Sophisticated gradient backgrounds with depth
|
||||
- SVG charts and data visualizations inline
|
||||
- Glassmorphism and neumorphism effects
|
||||
- Professional typography with Inter/SF Pro fonts
|
||||
- Strategic use of whitespace
|
||||
- Micro-animations on hover/focus states
|
||||
- Progress indicators and visual hierarchy
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Compelling Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Plain text content summary",
|
||||
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN SYSTEM:
|
||||
- Primary: ${brandColors[0] || primaryColor}
|
||||
- Secondary: ${brandColors[1] || secondaryColor}
|
||||
- Background: ${bgColor}
|
||||
- Text: ${textColor}${brandColorStr}
|
||||
|
||||
ANIMATION STYLE: ${animationStyle}
|
||||
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||
|
||||
CSS ANIMATIONS TO INCLUDE:
|
||||
\`\`\`css
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||
\`\`\`
|
||||
|
||||
SLIDE TYPES TO CREATE:
|
||||
1. TITLE SLIDE: Hero-style with animated gradient background, large typography
|
||||
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||
4. KEY METRICS: Large animated numbers with KPI cards
|
||||
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||
7. QUOTE: Large typography with decorative quote marks
|
||||
8. CALL-TO-ACTION: Bold CTA with pulsing button effect
|
||||
|
||||
TARGET AUDIENCE: ${audience}
|
||||
AUDIENCE STYLE: ${audienceStyle}
|
||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||
|
||||
REQUIREMENTS:
|
||||
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
|
||||
- ALL content in ${language}
|
||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||
- Use animation-delay for staggered reveal effects
|
||||
- Include decorative background elements (gradients, shapes)
|
||||
- Ensure text contrast meets WCAG AA standards
|
||||
- Add subtle shadow/glow effects for depth`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a STUNNING, ANIMATED presentation about:
|
||||
|
||||
${topic}
|
||||
|
||||
SPECIFICATIONS:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Slides: ${slideCount}
|
||||
- Audience: ${audience} (${audienceStyle})
|
||||
- Animation Style: ${animationStyle}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||
|
||||
Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients, and corporate-ready design!`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
specialInstructions?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English",
|
||||
specialInstructions = ""
|
||||
} = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an EXPERT Google Ads strategist. Create HIGH-CONVERTING campaigns with comprehensive keyword research, compelling ad copy, and optimized campaign structures.
|
||||
|
||||
CRITICAL ACCURACY PROTOCOL:
|
||||
1. STRICT ADHERENCE TO FACTS: Use ONLY locations, contact info, and services explicitly mentioned in the provided Website URL or Products list.
|
||||
2. DO NOT HALLUCINATE LOCATIONS: If no specific location is provided, default to "National" or "Global" based on the URL TLD (e.g. .co.uk -> UK). DO NOT invent cities or streets.
|
||||
3. COMPREHENSIVE OUTPUT: You MUST generate full lists (15+ keywords, 3+ ad variations). Do not truncate.
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"keywords": {
|
||||
"primary": [{"keyword": "term", "type": "primary", "searchVolume": 12000, "competition": "medium", "cpc": "$2.50"}],
|
||||
"longTail": [{"keyword": "specific term", "type": "long-tail", "searchVolume": 1200, "competition": "low", "cpc": "$1.25"}],
|
||||
"negative": [{"keyword": "exclude term", "type": "negative", "competition": "low"}]
|
||||
},
|
||||
"adCopies": [{
|
||||
"id": "ad-1",
|
||||
"campaignType": "search",
|
||||
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||
"callToAction": "Get Started",
|
||||
"mobileOptimized": true,
|
||||
"positioning": "Value proposition used"
|
||||
}],
|
||||
"campaigns": [{
|
||||
"id": "campaign-1",
|
||||
"name": "Campaign Name",
|
||||
"type": "search",
|
||||
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||
"biddingStrategy": "Maximize conversions",
|
||||
"targeting": {
|
||||
"locations": ["Specific regions/cities"],
|
||||
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
|
||||
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
|
||||
"schedule": ["Mon-Fri, 9am-5pm"]
|
||||
},
|
||||
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
|
||||
}],
|
||||
"implementation": {
|
||||
"setupSteps": ["Step 1", "Step 2"],
|
||||
"qualityScoreTips": ["Tip 1", "Tip 2"],
|
||||
"trackingSetup": ["Conversion tag info", "GTM setup"],
|
||||
"optimizationTips": ["Tip 1", "Tip 2"]
|
||||
},
|
||||
"predictions": {
|
||||
"estimatedClicks": "500-800",
|
||||
"estimatedImpressions": "15,000-25,000",
|
||||
"estimatedCtr": "3.5%",
|
||||
"estimatedConversions": "30-50",
|
||||
"conversionRate": "4.2%",
|
||||
"avgCpc": "$1.85"
|
||||
},
|
||||
"historicalBenchmarks": {
|
||||
"industryAverageCtr": "3.1%",
|
||||
"industryAverageCpc": "$2.10",
|
||||
"seasonalTrends": "Peak in Q4",
|
||||
"geographicInsights": "London/NY show highest ROI"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Requirements:
|
||||
- 10-15 primary keywords, 15-20 long-tail, 5-10 negative
|
||||
- Headlines max 30 chars, descriptions max 90 chars
|
||||
- 3-5 ad variations per campaign
|
||||
- 3-5 ad variations per campaign
|
||||
- Include budget and targeting recommendations
|
||||
- ENSURE ALL LISTS ARE POPULATED. No empty arrays.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a Google Ads campaign for:
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
PRODUCTS/SERVICES: ${productsServices.join(", ")}
|
||||
TARGET AUDIENCE: ${targetAudience}
|
||||
INDUSTRY: ${industry}
|
||||
LANGUAGE: ${language}
|
||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||
|
||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.
|
||||
STRICTLY FOLLOW LOCALIZATION: Use only locations relevant to the provided website. Do not invent office locations.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateMagicWand(
|
||||
websiteUrl: string,
|
||||
product: string,
|
||||
budget: number,
|
||||
specialInstructions?: string,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy.
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"marketAnalysis": {
|
||||
"industrySize": "Estimated market size",
|
||||
"growthRate": "Annual growth percentage",
|
||||
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||
},
|
||||
"competitorInsights": [
|
||||
{
|
||||
"competitor": "Competitor Name",
|
||||
"website": "URL",
|
||||
"estimatedSpend": "$10k-$50k/mo",
|
||||
"targetAudience": "Who they target",
|
||||
"strengths": ["Strength 1", "Strength 2"],
|
||||
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||
"adStrategy": "Their approach",
|
||||
"topKeywords": ["keyword 1", "keyword 2"],
|
||||
"adCopyExamples": [
|
||||
{"headline": "Example Headline", "description": "Example Description"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"strategies": [
|
||||
{
|
||||
"id": "strategy-1",
|
||||
"direction": "Strategic Direction Name",
|
||||
"rationale": "Why this strategy works",
|
||||
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
|
||||
"targetingDetails": {
|
||||
"geography": "Primary locations",
|
||||
"demographics": "Specific age/gender groups",
|
||||
"behavior": "User behaviors"
|
||||
},
|
||||
"competitiveAdvantage": "How this beats competitors",
|
||||
"messagingPillars": ["Pillar 1", "Pillar 2"],
|
||||
"keyMessages": ["Message 1", "Message 2"],
|
||||
"adCopyGuide": {
|
||||
"headlines": ["Headline 1", "Headline 2"],
|
||||
"descriptions": ["Description 1", "Description 2"],
|
||||
"keywords": ["keyword 1", "keyword 2"],
|
||||
"setupGuide": "Step-by-step for Google Ads Manager"
|
||||
},
|
||||
"recommendedChannels": ["Search", "Display", "YouTube"],
|
||||
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||
"expectedROI": "150-200%",
|
||||
"riskLevel": "low",
|
||||
"timeToResults": "2-3 months",
|
||||
"successMetrics": ["CTR > 3%", "CPA < $20"]
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
- Provide 5-7 DISTINCT strategic directions
|
||||
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||
- Include REAL competitive insights based on industry knowledge
|
||||
- Risk levels: "low", "medium", or "high"
|
||||
- AD COPY GUIDE must be incredibly "noob-friendly" - explain exactly where to paste each field in Google Ads Manager
|
||||
- Headlines MUST be under 30 characters
|
||||
- Descriptions MUST be under 90 characters`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
PRODUCT/SERVICE: ${product}
|
||||
MONTHLY BUDGET: $${budget}
|
||||
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||
|
||||
Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateMarketResearch(
|
||||
options: {
|
||||
websiteUrl: string;
|
||||
additionalUrls?: string[];
|
||||
competitors: string[];
|
||||
productMapping: string;
|
||||
specialInstructions?: string;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const { websiteUrl, additionalUrls = [], competitors = [], productMapping, specialInstructions = "" } = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS Market Research Analyst. Perform a deep-dive automated market analysis.
|
||||
|
||||
OUTPUT FORMAT - JSON:
|
||||
{
|
||||
"executiveSummary": "findings",
|
||||
"priceComparisonMatrix": [
|
||||
{ "product": "P", "userPrice": "$", "competitorPrices": [{ "competitor": "C", "price": "$", "url": "link" }] }
|
||||
],
|
||||
"featureComparisonTable": [
|
||||
{ "feature": "F", "userStatus": "status", "competitorStatus": [{ "competitor": "C", "status": "status" }] }
|
||||
],
|
||||
"marketPositioning": { "landscape": "LS", "segmentation": "SG" },
|
||||
"competitiveAnalysis": { "advantages": [], "disadvantages": [] },
|
||||
"recommendations": [],
|
||||
"methodology": "method"
|
||||
}
|
||||
|
||||
REQUIREMENTS: Use provided URLs. Be realistic.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `🔬 MARKET RESEARCH REQUEST 🔬
|
||||
WEBSITE: ${websiteUrl}
|
||||
PAGES: ${additionalUrls.join(", ")}
|
||||
COMPETITORS: ${competitors.join(", ")}
|
||||
MAPPING: ${productMapping}
|
||||
${specialInstructions ? `CUSTOM: ${specialInstructions}` : ""}
|
||||
|
||||
Perform analysis based on provided instructions.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateAIAssist(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const systemPrompt = `You are "AI Assist". Help conversationally.
|
||||
Switch agents if needed (content, seo, smm, pm, code, design, web, app).
|
||||
Output JSON for previews or agent switches:
|
||||
{ "content": "text", "agent": "id", "preview": { "type": "code|design|content|seo", "data": "...", "language": "..." } }`;
|
||||
|
||||
const chatMessages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...options.messages.map(m => ({
|
||||
role: m.role as "user" | "assistant" | "system",
|
||||
content: m.content
|
||||
}))
|
||||
];
|
||||
return { success: true, data: models };
|
||||
|
||||
return await this.chatCompletion(chatMessages, model || this.getAvailableModels()[0]);
|
||||
}
|
||||
|
||||
async generateAIAssistStream(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
onChunk: (chunk: string) => void;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<void>> {
|
||||
try {
|
||||
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
|
||||
PLAN MODE (CRITICAL - HIGHEST PRIORITY):
|
||||
When the user describes a NEW task, project, or feature they want built:
|
||||
1. DO NOT generate any code, [PREVIEW] tags, or implementation details.
|
||||
2. Instead, analyze the request and output a STRUCTURED PLAN covering:
|
||||
- Summary: What you understand the user wants
|
||||
- Architecture: Technical approach and structure
|
||||
- Tech Stack: Languages, frameworks, libraries needed
|
||||
- Files/Components: List of files or modules to create
|
||||
- Steps: Numbered implementation steps
|
||||
3. Format the plan in clean Markdown with headers and bullet points.
|
||||
4. Keep plans concise but thorough. Focus on the WHAT and HOW, not the actual code.
|
||||
5. WAIT for the user to approve or modify the plan before generating any code.
|
||||
|
||||
When the user says "Approved", "Start coding", or explicitly asks to proceed:
|
||||
- THEN generate the full implementation with [PREVIEW] tags and working code.
|
||||
- Follow the approved plan exactly.
|
||||
|
||||
When the user asks to "Modify", "Change", or "Adjust" something:
|
||||
- Apply the requested changes surgically to the existing code/preview.
|
||||
- Output updated [PREVIEW] with the full modified code.
|
||||
|
||||
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- 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.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
- design: UI/UX Designer. Create high-fidelity mockups and components.
|
||||
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
|
||||
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
|
||||
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
|
||||
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
|
||||
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
|
||||
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||
|
||||
BACKEND LOGIC & SIMULATION:
|
||||
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||
|
||||
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||
- DO NOT regenerate the entire design if it was not requested.
|
||||
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
|
||||
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
|
||||
|
||||
CANVAS MODE:
|
||||
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
|
||||
- Inside [PREVIEW], output ONLY the actual code or structured data.
|
||||
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
|
||||
|
||||
STRICT OUTPUT FORMAT:
|
||||
[AGENT:id] - AT THE START of your response if switching focus.
|
||||
[PREVIEW:type:language]
|
||||
ACTUAL_FUNCTIONAL_CODE_OR_DATA
|
||||
[/PREVIEW]
|
||||
|
||||
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
|
||||
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
|
||||
- The change log should appear in the CHAT, NOT inside the preview code.
|
||||
- Example format:
|
||||
[/PREVIEW]
|
||||
|
||||
**Change Log:**
|
||||
- Added feature X
|
||||
- Modified component Y
|
||||
- Fixed issue Z
|
||||
|
||||
IMPORTANT: IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...options.messages.map(m => ({
|
||||
role: m.role as "user" | "assistant" | "system",
|
||||
content: m.content
|
||||
}))
|
||||
];
|
||||
|
||||
// Call our local proxy to avoid CORS
|
||||
const headers = await this.getRequestHeaders();
|
||||
const baseUrl = this.getEffectiveEndpoint();
|
||||
const url = `${this.oauthBaseUrl}/chat`;
|
||||
|
||||
console.log("[QwenOAuth] Stream request (via proxy):", { url, model: model || this.getAvailableModels()[0], hasAuth: !!headers.Authorization });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: headers.Authorization || "",
|
||||
},
|
||||
signal: options.signal,
|
||||
body: JSON.stringify({
|
||||
endpoint: baseUrl,
|
||||
model: model || this.getAvailableModels()[0],
|
||||
messages,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[QwenOAuth] Stream proxy request failed:", response.status, errorText);
|
||||
throw new Error(`Stream request failed (${response.status}): ${errorText.slice(0, 200)}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error("No reader available");
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
buffer += chunk;
|
||||
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine || !trimmedLine.startsWith("data:")) continue;
|
||||
|
||||
const dataStr = trimmedLine.replace(/^data:\s*/, "");
|
||||
if (dataStr === "[DONE]") break;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(dataStr);
|
||||
if (data.choices?.[0]?.delta?.content) {
|
||||
options.onChunk(data.choices[0].delta.content);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors for incomplete lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
} catch (error) {
|
||||
console.error("[QwenOAuth] Stream error:", error);
|
||||
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
|
||||
}
|
||||
}
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
return { success: true, data: this.getAvailableModels() };
|
||||
}
|
||||
|
||||
getAvailableModels(): string[] {
|
||||
@@ -647,4 +1291,5 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
||||
|
||||
const qwenOAuthService = new QwenOAuthService();
|
||||
export default qwenOAuthService;
|
||||
export { qwenOAuthService };
|
||||
|
||||
|
||||
|
||||
66
lib/services/search-api.ts
Normal file
66
lib/services/search-api.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Web search API wrapper using SearXNG public instances.
|
||||
* No API key required — free for server-side use.
|
||||
*/
|
||||
|
||||
export interface SearchResult {
|
||||
title: string;
|
||||
url: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
const SEARXNG_INSTANCES = [
|
||||
"https://searx.be",
|
||||
"https://search.sapti.me",
|
||||
"https://searx.tiekoetter.com",
|
||||
"https://search.bus-hit.me",
|
||||
];
|
||||
|
||||
async function searchSearXNG(query: string): Promise<SearchResult[]> {
|
||||
for (const instance of SEARXNG_INSTANCES) {
|
||||
try {
|
||||
const url = `${instance}/search?q=${encodeURIComponent(query)}&format=json&categories=general&language=en`;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"User-Agent": "PromptArch/1.4 (https://rommark.dev)",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!res.ok) continue;
|
||||
|
||||
const data = await res.json();
|
||||
const results: SearchResult[] = (data.results || [])
|
||||
.slice(0, 8)
|
||||
.map((r: Record<string, string>) => ({
|
||||
title: r.title || "",
|
||||
url: r.url || "",
|
||||
snippet: r.content || "",
|
||||
}))
|
||||
.filter((r: SearchResult) => r.title && r.url);
|
||||
|
||||
if (results.length > 0) return results;
|
||||
} catch {
|
||||
// Try next instance
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function searchWeb(query: string): Promise<SearchResult[]> {
|
||||
// Clean the query — take first meaningful line, max 200 chars
|
||||
const cleanQuery = query
|
||||
.split("\n")[0]
|
||||
.replace(/\[.*?\]/g, "")
|
||||
.trim()
|
||||
.substring(0, 200);
|
||||
|
||||
if (!cleanQuery || cleanQuery.length < 3) return [];
|
||||
|
||||
return searchSearXNG(cleanQuery);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChatMessage, APIResponse } from "@/types";
|
||||
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||
|
||||
export interface ZaiPlanConfig {
|
||||
apiKey?: string;
|
||||
@@ -21,6 +21,37 @@ export class ZaiPlanService {
|
||||
return !!this.config.apiKey;
|
||||
}
|
||||
|
||||
async validateConnection(): Promise<APIResponse<{ valid: boolean; models?: string[] }>> {
|
||||
try {
|
||||
if (!this.config.apiKey) {
|
||||
return { success: false, error: "API key is required" };
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.config.generalEndpoint}/models`, {
|
||||
method: "GET",
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
return { success: false, error: "Invalid API key" };
|
||||
}
|
||||
return { success: false, error: `Connection failed (${response.status}): ${response.statusText}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data?.map((m: any) => m.id) || [];
|
||||
|
||||
return { success: true, data: { valid: true, models } };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Connection failed";
|
||||
if (message.includes("fetch")) {
|
||||
return { success: false, error: "Network error - check your internet connection" };
|
||||
}
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
private getHeaders(): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
@@ -63,7 +94,7 @@ export class ZaiPlanService {
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Z.AI] Response data:", data);
|
||||
|
||||
|
||||
if (data.choices && data.choices[0] && data.choices[0].message) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
} else if (data.output && data.output.choices && data.output.choices[0]) {
|
||||
@@ -80,51 +111,82 @@ export class ZaiPlanService {
|
||||
}
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
|
||||
async enhancePrompt(prompt: string, model?: string, options?: { toolCategory?: string; template?: string; diagnostics?: string }): Promise<APIResponse<string>> {
|
||||
const toolCategory = options?.toolCategory || 'reasoning';
|
||||
const template = options?.template || 'rtf';
|
||||
const diagnostics = options?.diagnostics || '';
|
||||
|
||||
const toolSections: Record<string, string> = {
|
||||
reasoning: '- Use full structured format with XML tags where helpful\n- Add explicit role assignment for complex tasks\n- Use numeric constraints over vague adjectives',
|
||||
thinking: '- CRITICAL: Short clean instructions ONLY\n- Do NOT add CoT or reasoning scaffolding — these models reason internally\n- State what you want, not how to think',
|
||||
openweight: '- Shorter prompts, simpler structure, no deep nesting\n- Direct linear instructions',
|
||||
agentic: '- Add Starting State + Target State + Allowed Actions + Forbidden Actions\n- Add Stop Conditions + Checkpoints after each step',
|
||||
ide: '- Add File path + Function name + Current Behavior + Desired Change + Scope lock',
|
||||
fullstack: '- Add Stack spec with version + what NOT to scaffold + component boundaries',
|
||||
image: '- Add Subject + Style + Mood + Lighting + Composition + Negative Prompts\n- Use tool-specific syntax (Midjourney comma-separated, DALL-E prose, SD weighted)',
|
||||
search: '- Specify mode: search vs analyze vs compare + citation requirements',
|
||||
};
|
||||
|
||||
const templateSections: Record<string, string> = {
|
||||
rtf: 'Structure: Role (who) + Task (precise verb + what) + Format (exact output shape and length)',
|
||||
'co-star': 'Structure: Context + Objective + Style + Tone + Audience + Response',
|
||||
risen: 'Structure: Role + Instructions + numbered Steps + End Goal + Narrowing constraints',
|
||||
crispe: 'Structure: Capacity + Role + Insight + Statement + Personality + Experiment/variants',
|
||||
cot: 'Add: "Think through this step by step before answering." Only for standard reasoning models, NOT for o1/o3/R1.',
|
||||
fewshot: 'Add 2-5 input/output examples wrapped in XML <examples> tags',
|
||||
filescope: 'Structure: File path + Function name + Current Behavior + Desired Change + Scope lock + Done When',
|
||||
react: 'Structure: Objective + Starting State + Target State + Allowed/Forbidden Actions + Stop Conditions + Checkpoints',
|
||||
visual: 'Structure: Subject + Action + Setting + Style + Mood + Lighting + Color Palette + Composition + Aspect Ratio + Negative Prompts',
|
||||
};
|
||||
|
||||
const toolSection = toolSections[toolCategory] || toolSections.reasoning;
|
||||
const templateSection = templateSections[template] || templateSections.rtf;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an expert prompt engineer. Your task is to enhance user prompts to make them more precise, actionable, and effective for AI coding agents.
|
||||
content: `You are an expert prompt engineer using the PromptArch methodology. Enhance the user\'s prompt to be production-ready.
|
||||
|
||||
Apply these principles:
|
||||
1. Add specific context about project and requirements
|
||||
2. Clarify constraints and preferences
|
||||
3. Define expected output format clearly
|
||||
4. Include edge cases and error handling requirements
|
||||
5. Specify testing and validation criteria
|
||||
STEP 1 — DIAGNOSE AND FIX these failure patterns:
|
||||
- Vague task verb -> replace with precise operation
|
||||
- Two tasks in one -> keep primary task, note the split
|
||||
- No success criteria -> add "Done when: [specific measurable condition]"
|
||||
- Missing output format -> add explicit format lock (structure, length, type)
|
||||
- No role assignment (complex tasks) -> add domain-specific expert identity
|
||||
- Vague aesthetic ("professional", "clean") -> concrete measurable specs
|
||||
- No scope boundary -> add explicit scope lock
|
||||
- Over-permissive language -> add constraints and boundaries
|
||||
- Emotional description -> extract specific technical fault
|
||||
- Implicit references -> restate fully
|
||||
- No grounding for factual tasks -> add certainty constraint
|
||||
- No CoT for logic tasks -> add step-by-step reasoning
|
||||
|
||||
Return ONLY the enhanced prompt, no explanations or extra text.`,
|
||||
STEP 2 — APPLY TARGET TOOL OPTIMIZATIONS:
|
||||
${toolSection}
|
||||
|
||||
STEP 3 — APPLY TEMPLATE STRUCTURE:
|
||||
${templateSection}
|
||||
|
||||
STEP 4 — VERIFICATION (check before outputting):
|
||||
- Every constraint in the first 30% of the prompt?
|
||||
- MUST/NEVER over should/avoid?
|
||||
- Every sentence load-bearing with zero padding?
|
||||
- Format explicit with stated length?
|
||||
- Scope bounded?
|
||||
- Would this produce correct output on first try?
|
||||
|
||||
STEP 5 — OUTPUT:
|
||||
Output ONLY the enhanced prompt. No explanations, no commentary, no markdown code fences.
|
||||
The prompt must be ready to paste directly into the target AI tool.${diagnostics ? '\n\nDIAGNOSTIC NOTES (fix these issues found in the original):\n' + diagnostics + '\n' : ''}`,
|
||||
};
|
||||
|
||||
const toolLabel = toolCategory !== 'reasoning' ? ` for ${toolCategory} AI tool` : '';
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
|
||||
content: `Enhance this prompt${toolLabel}:\n\n${prompt}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an expert product manager and technical architect. Generate a comprehensive Product Requirements Document (PRD) based on user's idea.
|
||||
|
||||
Structure your PRD with these sections:
|
||||
1. Overview & Objectives
|
||||
2. User Personas & Use Cases
|
||||
3. Functional Requirements (prioritized by importance)
|
||||
4. Non-functional Requirements
|
||||
5. Technical Architecture Recommendations
|
||||
6. Success Metrics & KPIs
|
||||
|
||||
Use clear, specific language suitable for development teams.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Generate a PRD for this idea:\n\n${idea}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7");
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "${default_model}");
|
||||
}
|
||||
|
||||
async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> {
|
||||
@@ -168,7 +230,7 @@ Include specific recommendations for:
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data?.map((m: any) => m.id) || [];
|
||||
|
||||
|
||||
return { success: true, data: models };
|
||||
} else {
|
||||
console.log("[Z.AI] No API key, using fallback models");
|
||||
@@ -251,6 +313,676 @@ Make the prompt specific, inspiring, and comprehensive. Use professional UX term
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
animationStyle?: string;
|
||||
audienceStyle?: string;
|
||||
themeColors?: string[];
|
||||
brandColors?: string[];
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
language = "English",
|
||||
theme = "executive-dark",
|
||||
slideCount = 10,
|
||||
audience = "Executives & C-Suite",
|
||||
organization = "",
|
||||
animationStyle = "Professional",
|
||||
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||
brandColors = []
|
||||
} = options;
|
||||
|
||||
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||
const brandColorStr = brandColors.length > 0
|
||||
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||
: "";
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||
|
||||
Your slides must be VISUALLY SPECTACULAR with:
|
||||
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||
- Sophisticated gradient backgrounds with depth
|
||||
- SVG charts and data visualizations inline
|
||||
- Glassmorphism and neumorphism effects
|
||||
- Professional typography with Inter/SF Pro fonts
|
||||
- Strategic use of whitespace
|
||||
- Micro-animations on hover/focus states
|
||||
- Progress indicators and visual hierarchy
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Compelling Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Plain text content summary",
|
||||
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN SYSTEM:
|
||||
- Primary: ${brandColors[0] || primaryColor}
|
||||
- Secondary: ${brandColors[1] || secondaryColor}
|
||||
- Background: ${bgColor}
|
||||
- Text: ${textColor}${brandColorStr}
|
||||
|
||||
ANIMATION STYLE: ${animationStyle}
|
||||
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||
|
||||
CSS ANIMATIONS TO INCLUDE:
|
||||
\`\`\`css
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
|
||||
@keyframes gradientShift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
\`\`\`
|
||||
|
||||
SLIDE TYPES TO CREATE:
|
||||
1. TITLE SLIDE: Hero-style with animated gradient background, large typography, subtle floating elements
|
||||
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||
4. KEY METRICS: Large animated numbers with counting effect styling, KPI cards with glassmorphism
|
||||
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||
7. QUOTE: Large typography with decorative quote marks, subtle background pattern
|
||||
8. CALL-TO-ACTION: Bold CTA with pulsing button effect, clear next steps
|
||||
|
||||
SVG CHART EXAMPLE:
|
||||
\`\`\`html
|
||||
<svg viewBox="0 0 400 200" style="width:100%;max-width:400px;">
|
||||
<defs>
|
||||
<linearGradient id="barGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${primaryColor}"/>
|
||||
<stop offset="100%" style="stop-color:${secondaryColor}"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="50" y="50" width="60" height="130" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.2s both; transform-origin: bottom;"/>
|
||||
<rect x="130" y="80" width="60" height="100" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.4s both; transform-origin: bottom;"/>
|
||||
<rect x="210" y="30" width="60" height="150" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.6s both; transform-origin: bottom;"/>
|
||||
</svg>
|
||||
\`\`\`
|
||||
|
||||
TARGET AUDIENCE: ${audience}
|
||||
AUDIENCE STYLE: ${audienceStyle}
|
||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||
|
||||
REQUIREMENTS:
|
||||
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
|
||||
- ALL content in ${language}
|
||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||
- Use animation-delay for staggered reveal effects
|
||||
- Include decorative background elements (gradients, shapes, patterns)
|
||||
- Ensure text contrast meets WCAG AA standards
|
||||
- Add subtle shadow/glow effects for depth
|
||||
- Include progress/slide number indicator styling`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a STUNNING, ANIMATED presentation about:
|
||||
|
||||
${topic}
|
||||
|
||||
SPECIFICATIONS:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Slides: ${slideCount}
|
||||
- Audience: ${audience} (${audienceStyle})
|
||||
- Animation Style: ${animationStyle}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||
|
||||
Generate SPECTACULAR slides with:
|
||||
✨ Animated CSS3 transitions and keyframes
|
||||
📊 SVG charts and data visualizations where relevant
|
||||
🎨 Modern gradients and glassmorphism effects
|
||||
💫 Staggered reveal animations
|
||||
🏢 Corporate-ready, executive-level design
|
||||
|
||||
Return the complete JSON with full htmlContent for each slide. Make each slide VISUALLY IMPRESSIVE and memorable!`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
specialInstructions?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English",
|
||||
specialInstructions = ""
|
||||
} = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an EXPERT Google Ads strategist with 15+ years of experience managing $100M+ in ad spend. You create HIGH-CONVERTING campaigns that consistently outperform industry benchmarks.
|
||||
|
||||
Your expertise includes:
|
||||
- Keyword research and competitive analysis
|
||||
- Ad copywriting that drives clicks and conversions
|
||||
- Campaign structure optimization
|
||||
- Quality Score improvement strategies
|
||||
- ROI maximization techniques
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"keywords": {
|
||||
"primary": [{"keyword": "term", "type": "primary", "searchVolume": 12000, "competition": "medium", "cpc": "$2.50"}],
|
||||
"longTail": [{"keyword": "specific term", "type": "long-tail", "searchVolume": 1200, "competition": "low", "cpc": "$1.25"}],
|
||||
"negative": [{"keyword": "exclude term", "type": "negative", "competition": "low"}]
|
||||
},
|
||||
"adCopies": [{
|
||||
"id": "ad-1",
|
||||
"campaignType": "search",
|
||||
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||
"callToAction": "Get Started",
|
||||
"mobileOptimized": true,
|
||||
"positioning": "Value proposition used"
|
||||
}],
|
||||
"campaigns": [{
|
||||
"id": "campaign-1",
|
||||
"name": "Campaign Name",
|
||||
"type": "search",
|
||||
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||
"biddingStrategy": "Maximize conversions",
|
||||
"targeting": {
|
||||
"locations": ["Specific regions/cities"],
|
||||
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
|
||||
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
|
||||
"schedule": ["Mon-Fri, 9am-5pm"]
|
||||
},
|
||||
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
|
||||
}],
|
||||
"implementation": {
|
||||
"setupSteps": ["Step 1", "Step 2"],
|
||||
"qualityScoreTips": ["Tip 1", "Tip 2"],
|
||||
"trackingSetup": ["Conversion tag info", "GTM setup"],
|
||||
"optimizationTips": ["Tip 1", "Tip 2"]
|
||||
},
|
||||
"predictions": {
|
||||
"estimatedClicks": "500-800",
|
||||
"estimatedImpressions": "15,000-25,000",
|
||||
"estimatedCtr": "3.5%",
|
||||
"estimatedConversions": "30-50",
|
||||
"conversionRate": "4.2%",
|
||||
"avgCpc": "$1.85"
|
||||
},
|
||||
"historicalBenchmarks": {
|
||||
"industryAverageCtr": "3.1%",
|
||||
"industryAverageCpc": "$2.10",
|
||||
"seasonalTrends": "Peak in Q4",
|
||||
"geographicInsights": "London/NY show highest ROI"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
KEYWORD RESEARCH REQUIREMENTS:
|
||||
- Generate 10-15 PRIMARY keywords (high-volume, highly relevant)
|
||||
- Generate 15-20 LONG-TAIL keywords (specific, lower-competition)
|
||||
- Generate 5-10 NEGATIVE keywords (terms to exclude)
|
||||
- Include realistic search volume estimates
|
||||
- Provide competition level and CPC estimates
|
||||
|
||||
AD COPY REQUIREMENTS:
|
||||
- Headlines MUST be 30 characters or less
|
||||
- Descriptions MUST be 90 characters or less
|
||||
- Create 3-5 unique ad variations per campaign type
|
||||
- Include strong calls-to-action
|
||||
- Focus on benefits and unique value propositions
|
||||
- Mobile-optimized versions required
|
||||
|
||||
CAMPAIGN STRUCTURE:
|
||||
- Organize by product/service theme
|
||||
- Recommend appropriate bidding strategies
|
||||
- Include targeting recommendations
|
||||
- Suggest budget allocation
|
||||
|
||||
QUALITY STANDARDS:
|
||||
- All keywords must be relevant (>85% match)
|
||||
- Ad copy must comply with Google Ads policies
|
||||
- No trademark violations
|
||||
- Professional, compelling language
|
||||
- Clear value propositions`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a COMPREHENSIVE Google Ads campaign for:
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
|
||||
PRODUCTS/SERVICES TO PROMOTE:
|
||||
${productsServices.map((p, i) => `${i + 1}. ${p}`).join("\n")}
|
||||
|
||||
TARGET AUDIENCE: ${targetAudience}
|
||||
INDUSTRY: ${industry}
|
||||
LANGUAGE: ${language}
|
||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||
|
||||
Generate a COMPLETE Google Ads package including:
|
||||
🔍 Comprehensive keyword research (primary, long-tail, negative)
|
||||
✍️ High-converting ad copy (multiple variations)
|
||||
📊 Optimized campaign structure
|
||||
📈 Performance predictions
|
||||
🎯 Implementation guidance
|
||||
|
||||
Make this campaign READY TO LAUNCH with copy-paste ready content!`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateMagicWand(
|
||||
websiteUrl: string,
|
||||
product: string,
|
||||
budget: number,
|
||||
specialInstructions?: string,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy. You have access to deep industry knowledge and can analyze markets like a Fortune 500 CMO.
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"marketAnalysis": {
|
||||
"industrySize": "Estimated market size",
|
||||
"growthRate": "Annual growth percentage",
|
||||
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||
},
|
||||
"competitorInsights": [
|
||||
{
|
||||
"competitor": "Competitor Name",
|
||||
"website": "URL",
|
||||
"estimatedSpend": "$10k-$50k/mo",
|
||||
"targetAudience": "Who they target",
|
||||
"strengths": ["Strength 1", "Strength 2"],
|
||||
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||
"adStrategy": "Their approach",
|
||||
"topKeywords": ["keyword 1", "keyword 2"],
|
||||
"adCopyExamples": [
|
||||
{"headline": "Example Headline", "description": "Example Description"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"strategies": [
|
||||
{
|
||||
"id": "strategy-1",
|
||||
"direction": "Strategic Direction Name",
|
||||
"rationale": "Why this strategy works",
|
||||
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
|
||||
"targetingDetails": {
|
||||
"geography": "Primary locations",
|
||||
"demographics": "Specific age/gender groups",
|
||||
"behavior": "User behaviors"
|
||||
},
|
||||
"competitiveAdvantage": "How this beats competitors",
|
||||
"messagingPillars": ["Pillar 1", "Pillar 2"],
|
||||
"keyMessages": ["Message 1", "Message 2"],
|
||||
"adCopyGuide": {
|
||||
"headlines": ["Headline 1", "Headline 2"],
|
||||
"descriptions": ["Description 1", "Description 2"],
|
||||
"keywords": ["keyword 1", "keyword 2"],
|
||||
"setupGuide": "Step-by-step for Google Ads Manager"
|
||||
},
|
||||
"recommendedChannels": ["Search", "Display", "YouTube"],
|
||||
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||
"expectedROI": "150-200%",
|
||||
"riskLevel": "low",
|
||||
"timeToResults": "2-3 months",
|
||||
"successMetrics": ["CTR > 3%", "CPA < $20"]
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
- Provide 5-7 DISTINCT strategic directions
|
||||
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||
- Include REAL competitive insights based on industry knowledge
|
||||
- Budget allocations must sum to 100%
|
||||
- Risk levels: "low", "medium", or "high"
|
||||
- AD COPY GUIDE must be incredibly "noob-friendly" - explain exactly where to paste each field in Google Ads Manager
|
||||
- Headlines MUST be under 30 characters
|
||||
- Descriptions MUST be under 90 characters
|
||||
- Be REALISTIC with ROI and timeline estimates`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
PRODUCT/SERVICE: ${product}
|
||||
MONTHLY BUDGET: $${budget}
|
||||
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||
|
||||
MISSION: Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions that will DOMINATE this market.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateMarketResearch(
|
||||
options: {
|
||||
websiteUrl: string;
|
||||
additionalUrls?: string[];
|
||||
competitors: string[];
|
||||
productMapping: string;
|
||||
specialInstructions?: string;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const { websiteUrl, additionalUrls = [], competitors = [], productMapping, specialInstructions = "" } = options;
|
||||
|
||||
const systemPrompt = `You are a WORLD-CLASS Market Research Analyst and Competitive Intelligence Expert.
|
||||
Focus on accuracy and actionable intelligence.
|
||||
|
||||
You MUST return your analysis in the following STRICT JSON format:
|
||||
{
|
||||
"executiveSummary": "A concise overview of the market landscape and key findings.",
|
||||
"priceComparisonMatrix": [
|
||||
{
|
||||
"product": "Product Name",
|
||||
"userPrice": "$XX.XX",
|
||||
"competitorPrices": [
|
||||
{ "competitor": "Competitor Name", "price": "$XX.XX", "url": "https://competitor.com/product-page" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"featureComparisonTable": [
|
||||
{
|
||||
"feature": "Feature Name",
|
||||
"userStatus": true/false/text,
|
||||
"competitorStatus": [
|
||||
{ "competitor": "Competitor Name", "status": true/false/text }
|
||||
]
|
||||
}
|
||||
],
|
||||
"marketPositioning": {
|
||||
"landscape": "Description of the current market state.",
|
||||
"segmentation": "Analysis of target customer segments."
|
||||
},
|
||||
"competitiveAnalysis": {
|
||||
"advantages": ["Point 1", "Point 2"],
|
||||
"disadvantages": ["Point 1", "Point 2"]
|
||||
},
|
||||
"recommendations": ["Actionable step 1", "Actionable step 2"],
|
||||
"methodology": "Brief description of the research process."
|
||||
}
|
||||
|
||||
Requirements:
|
||||
1. Base your analysis on realistic price and feature estimates.
|
||||
2. Focus on core technical/business value.
|
||||
3. Ensure JSON is valid.`;
|
||||
|
||||
const userMsg = `WEBSITE TO ANALYZE: ${options.websiteUrl}
|
||||
COMPETITOR URLS: ${options.competitors.join(', ')}
|
||||
PRODUCT/FEATURE MAPPING: ${options.productMapping}
|
||||
SPECIAL REQUESTS: ${options.specialInstructions || 'Perform comprehensive analysis'}
|
||||
|
||||
Provide a COMPREHENSIVE competitive intelligence analysis.`;
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userMsg }
|
||||
];
|
||||
|
||||
return await this.chatCompletion(messages, model || this.getAvailableModels()[0]);
|
||||
}
|
||||
|
||||
async generateAIAssist(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch.
|
||||
Your goal is to provide intelligent conversational support and switch to specialized agents when necessary.
|
||||
|
||||
CURRENT SPECIALIZED AGENTS:
|
||||
- content, seo, smm, pm, code, design, web, app
|
||||
|
||||
STRICT OUTPUT FORMAT:
|
||||
You MUST respond in JSON format if you want to activate a preview or switch agents.
|
||||
{
|
||||
"content": "Your natural language response here...",
|
||||
"agent": "agent_id_to_switch_to (optional)",
|
||||
"preview": { // (optional)
|
||||
"type": "code" | "design" | "content" | "seo",
|
||||
"data": "The actual code, layout, or content to preview",
|
||||
"language": "javascript/html/css/markdown (optional)"
|
||||
}
|
||||
}
|
||||
|
||||
ROUTING LOGIC:
|
||||
- Automatically detect user intent and switch agents if appropriate.
|
||||
- Provide deep technical or creative output based on the active agent.
|
||||
|
||||
PREVIEW GUIDELINES:
|
||||
- Provide full code for 'web'/'app'/'code'.
|
||||
- Provide structured analysis for 'seo'/'content'.`;
|
||||
|
||||
const chatMessages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...options.messages.map(m => ({
|
||||
role: m.role as "user" | "assistant" | "system",
|
||||
content: m.content
|
||||
}))
|
||||
];
|
||||
|
||||
return await this.chatCompletion(chatMessages, model || this.getAvailableModels()[0]);
|
||||
}
|
||||
|
||||
async generateAIAssistStream(
|
||||
options: {
|
||||
messages: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
onChunk: (chunk: string) => void;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
model?: string
|
||||
): Promise<APIResponse<void>> {
|
||||
try {
|
||||
if (!this.config.apiKey) {
|
||||
throw new Error("API key is required.");
|
||||
}
|
||||
|
||||
// ... existing prompt logic ...
|
||||
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
|
||||
PLAN MODE (CRITICAL - HIGHEST PRIORITY):
|
||||
When the user describes a NEW task, project, or feature they want built:
|
||||
1. DO NOT generate any code, [PREVIEW] tags, or implementation details.
|
||||
2. Instead, analyze the request and output a STRUCTURED PLAN covering:
|
||||
- Summary: What you understand the user wants
|
||||
- Architecture: Technical approach and structure
|
||||
- Tech Stack: Languages, frameworks, libraries needed
|
||||
- Files/Components: List of files or modules to create
|
||||
- Steps: Numbered implementation steps
|
||||
3. Format the plan in clean Markdown with headers and bullet points.
|
||||
4. Keep plans concise but thorough. Focus on the WHAT and HOW, not the actual code.
|
||||
5. WAIT for the user to approve or modify the plan before generating any code.
|
||||
|
||||
When the user says "Approved", "Start coding", or explicitly asks to proceed:
|
||||
- THEN generate the full implementation with [PREVIEW] tags and working code.
|
||||
- Follow the approved plan exactly.
|
||||
|
||||
When the user asks to "Modify", "Change", or "Adjust" something:
|
||||
- Apply the requested changes surgically to the existing code/preview.
|
||||
- Output updated [PREVIEW] with the full modified code.
|
||||
|
||||
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- 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.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
- design: UI/UX Designer. Create high-fidelity mockups and components.
|
||||
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
|
||||
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
|
||||
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
|
||||
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
|
||||
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
|
||||
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||
|
||||
BACKEND LOGIC & SIMULATION:
|
||||
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||
|
||||
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||
- DO NOT regenerate the entire design if it was not requested.
|
||||
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
|
||||
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
|
||||
|
||||
CANVAS MODE:
|
||||
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
|
||||
- Inside [PREVIEW], output ONLY the actual code or structured data.
|
||||
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
|
||||
|
||||
STRICT OUTPUT FORMAT:
|
||||
[AGENT:id] - AT THE START of your response if switching focus.
|
||||
[PREVIEW:type:language]
|
||||
ACTUAL_FUNCTIONAL_CODE_OR_DATA
|
||||
[/PREVIEW]
|
||||
|
||||
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
|
||||
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
|
||||
- The change log should appear in the CHAT, NOT inside the preview code.
|
||||
- Example format:
|
||||
[/PREVIEW]
|
||||
|
||||
**Change Log:**
|
||||
- Added feature X
|
||||
- Modified component Y
|
||||
- Fixed issue Z
|
||||
|
||||
IMPORTANT: IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...options.messages.map(m => ({
|
||||
role: m.role as "user" | "assistant" | "system",
|
||||
content: m.content
|
||||
}))
|
||||
];
|
||||
|
||||
const endpoint = this.config.codingEndpoint; // AI Assist often involves coding
|
||||
const response = await fetch(`${endpoint}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
signal: options.signal,
|
||||
body: JSON.stringify({
|
||||
model: model || this.getAvailableModels()[0],
|
||||
messages,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Stream failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error("No reader");
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = chunk.split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line.trim() || !line.startsWith("data:")) continue;
|
||||
const dataStr = line.replace(/^data:\s*/, "");
|
||||
if (dataStr === "[DONE]") break;
|
||||
try {
|
||||
const data = JSON.parse(dataStr);
|
||||
const content = data.choices?.[0]?.delta?.content || data.output?.choices?.[0]?.delta?.content;
|
||||
if (content) options.onChunk(content);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ZaiPlanService;
|
||||
|
||||
134
lib/store.ts
134
lib/store.ts
@@ -1,20 +1,48 @@
|
||||
import { create } from "zustand";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan } from "@/types";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult, MarketResearchResult, AppView, AIAssistMessage } from "@/types";
|
||||
|
||||
interface AIAssistTab {
|
||||
id: string;
|
||||
title: string;
|
||||
history: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
previewData?: any | null;
|
||||
showCanvas?: boolean;
|
||||
}
|
||||
|
||||
interface ApiValidationStatus {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
lastValidated?: number;
|
||||
models?: string[];
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
currentPrompt: string;
|
||||
enhancedPrompt: string | null;
|
||||
prd: PRD | null;
|
||||
actionPlan: ActionPlan | null;
|
||||
slidesPresentation: SlidesPresentation | null;
|
||||
googleAdsResult: GoogleAdsResult | null;
|
||||
magicWandResult: MagicWandResult | null;
|
||||
marketResearchResult: MarketResearchResult | null;
|
||||
|
||||
// AI Assist Tabs
|
||||
aiAssistTabs: AIAssistTab[];
|
||||
activeTabId: string | null;
|
||||
|
||||
language: "en" | "ru" | "he";
|
||||
selectedProvider: ModelProvider;
|
||||
selectedModels: Record<ModelProvider, string>;
|
||||
availableModels: Record<ModelProvider, string[]>;
|
||||
apiKeys: Record<ModelProvider, string>;
|
||||
apiValidationStatus: Record<ModelProvider, ApiValidationStatus>;
|
||||
qwenTokens?: {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
} | null;
|
||||
githubToken?: string | null;
|
||||
isProcessing: boolean;
|
||||
error: string | null;
|
||||
history: {
|
||||
@@ -27,11 +55,27 @@ interface AppState {
|
||||
setEnhancedPrompt: (enhanced: string | null) => void;
|
||||
setPRD: (prd: PRD) => void;
|
||||
setActionPlan: (plan: ActionPlan) => void;
|
||||
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||
setMagicWandResult: (result: MagicWandResult | null) => void;
|
||||
setMarketResearchResult: (result: MarketResearchResult | null) => void;
|
||||
|
||||
// Tab Management
|
||||
setAIAssistTabs: (tabs: AIAssistTab[]) => void;
|
||||
setActiveTabId: (id: string | null) => void;
|
||||
addAIAssistTab: (agent?: string) => void;
|
||||
removeAIAssistTab: (id: string) => void;
|
||||
updateActiveTab: (updates: Partial<AIAssistTab>) => void;
|
||||
updateTabById: (tabId: string, updates: Partial<AIAssistTab>) => void;
|
||||
|
||||
setLanguage: (lang: "en" | "ru" | "he") => void;
|
||||
setSelectedProvider: (provider: ModelProvider) => void;
|
||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||
setApiKey: (provider: ModelProvider, key: string) => void;
|
||||
setApiValidationStatus: (provider: ModelProvider, status: ApiValidationStatus) => void;
|
||||
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
|
||||
setGithubToken: (token: string | null) => void;
|
||||
setProcessing: (processing: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
addToHistory: (prompt: string) => void;
|
||||
@@ -44,21 +88,45 @@ const useStore = create<AppState>((set) => ({
|
||||
enhancedPrompt: null,
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
googleAdsResult: null,
|
||||
magicWandResult: null,
|
||||
marketResearchResult: null,
|
||||
|
||||
aiAssistTabs: [{
|
||||
id: "default",
|
||||
title: "New Chat",
|
||||
history: [],
|
||||
currentAgent: "general"
|
||||
}],
|
||||
activeTabId: "default",
|
||||
|
||||
language: "en",
|
||||
selectedProvider: "qwen",
|
||||
selectedModels: {
|
||||
qwen: "coder-model",
|
||||
ollama: "gpt-oss:120b",
|
||||
zai: "glm-4.7",
|
||||
openrouter: "anthropic/claude-3.5-sonnet",
|
||||
},
|
||||
availableModels: {
|
||||
qwen: ["coder-model"],
|
||||
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
|
||||
zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||
openrouter: ["anthropic/claude-3.5-sonnet", "google/gemini-2.0-flash-exp:free", "meta-llama/llama-3.3-70b-instruct", "openai/gpt-4o-mini", "deepseek/deepseek-chat-v3-0324", "qwen/qwen-2.5-72b-instruct"],
|
||||
},
|
||||
apiKeys: {
|
||||
qwen: "",
|
||||
ollama: "",
|
||||
zai: "",
|
||||
openrouter: "",
|
||||
},
|
||||
githubToken: null,
|
||||
apiValidationStatus: {
|
||||
qwen: { valid: false },
|
||||
ollama: { valid: false },
|
||||
zai: { valid: false },
|
||||
openrouter: { valid: false },
|
||||
},
|
||||
isProcessing: false,
|
||||
error: null,
|
||||
@@ -68,6 +136,51 @@ const useStore = create<AppState>((set) => ({
|
||||
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
||||
setPRD: (prd) => set({ prd }),
|
||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||
setMagicWandResult: (result) => set({ magicWandResult: result }),
|
||||
setMarketResearchResult: (result) => set({ marketResearchResult: result }),
|
||||
|
||||
setAIAssistTabs: (tabs) => set({ aiAssistTabs: tabs }),
|
||||
setActiveTabId: (id) => set({ activeTabId: id }),
|
||||
addAIAssistTab: (agent = "general") => set((state) => {
|
||||
const newId = Math.random().toString(36).substr(2, 9);
|
||||
const newTab = {
|
||||
id: newId,
|
||||
title: `Chat ${state.aiAssistTabs.length + 1}`,
|
||||
history: [],
|
||||
currentAgent: agent,
|
||||
previewData: null,
|
||||
showCanvas: false
|
||||
};
|
||||
return {
|
||||
aiAssistTabs: [...state.aiAssistTabs, newTab],
|
||||
activeTabId: newId
|
||||
};
|
||||
}),
|
||||
removeAIAssistTab: (id) => set((state) => {
|
||||
const newTabs = state.aiAssistTabs.filter(t => t.id !== id);
|
||||
let nextActiveId = state.activeTabId;
|
||||
if (state.activeTabId === id) {
|
||||
nextActiveId = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
|
||||
}
|
||||
return {
|
||||
aiAssistTabs: newTabs,
|
||||
activeTabId: nextActiveId
|
||||
};
|
||||
}),
|
||||
updateActiveTab: (updates) => set((state) => ({
|
||||
aiAssistTabs: state.aiAssistTabs.map(t =>
|
||||
t.id === state.activeTabId ? { ...t, ...updates } : t
|
||||
)
|
||||
})),
|
||||
updateTabById: (tabId, updates) => set((state) => ({
|
||||
aiAssistTabs: state.aiAssistTabs.map(t =>
|
||||
t.id === tabId ? { ...t, ...updates } : t
|
||||
)
|
||||
})),
|
||||
|
||||
setLanguage: (lang) => set({ language: lang }),
|
||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||
setSelectedModel: (provider, model) =>
|
||||
set((state) => ({
|
||||
@@ -81,7 +194,15 @@ const useStore = create<AppState>((set) => ({
|
||||
set((state) => ({
|
||||
apiKeys: { ...state.apiKeys, [provider]: key },
|
||||
})),
|
||||
setApiValidationStatus: (provider, status) =>
|
||||
set((state) => ({
|
||||
apiValidationStatus: {
|
||||
...state.apiValidationStatus,
|
||||
[provider]: status,
|
||||
},
|
||||
})),
|
||||
setQwenTokens: (tokens) => set({ qwenTokens: tokens }),
|
||||
setGithubToken: (token) => set({ githubToken: token }),
|
||||
setProcessing: (processing) => set({ isProcessing: processing }),
|
||||
setError: (error) => set({ error }),
|
||||
addToHistory: (prompt) =>
|
||||
@@ -102,6 +223,17 @@ const useStore = create<AppState>((set) => ({
|
||||
enhancedPrompt: null,
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
googleAdsResult: null,
|
||||
magicWandResult: null,
|
||||
marketResearchResult: null,
|
||||
aiAssistTabs: [{
|
||||
id: "default",
|
||||
title: "New Chat",
|
||||
history: [],
|
||||
currentAgent: "general"
|
||||
}],
|
||||
activeTabId: "default",
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
517
package-lock.json
generated
517
package-lock.json
generated
@@ -9,13 +9,17 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.1",
|
||||
"postcss": "^8.4.49",
|
||||
@@ -29,10 +33,12 @@
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.2.1",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2"
|
||||
@@ -930,6 +936,294 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
@@ -1086,6 +1380,13 @@
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/file-saver": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
@@ -1145,7 +1446,7 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
@@ -1458,6 +1759,15 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -1952,6 +2262,19 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -2044,6 +2367,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
@@ -2059,6 +2394,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2102,6 +2446,24 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -3144,6 +3506,12 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -3206,6 +3574,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
@@ -3568,6 +3945,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
@@ -3603,6 +3986,12 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||
@@ -4177,6 +4566,18 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -4217,6 +4618,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
@@ -5549,6 +5959,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -5820,6 +6236,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -5972,6 +6394,27 @@
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -6250,6 +6693,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-push-apply": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||
@@ -6344,6 +6793,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
@@ -6514,6 +6969,18 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
@@ -6533,6 +7000,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string.prototype.includes": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||
@@ -7452,6 +7928,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -7461,6 +7955,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "promptarch",
|
||||
"version": "1.0.0",
|
||||
"version": "1.9.0",
|
||||
"description": "Transform vague ideas into production-ready prompts and PRDs",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -9,13 +9,17 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.1",
|
||||
"postcss": "^8.4.49",
|
||||
@@ -29,10 +33,12 @@
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.2.1",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2"
|
||||
@@ -46,14 +52,14 @@
|
||||
"ollama",
|
||||
"zai"
|
||||
],
|
||||
"author": "Roman | RyzenAdvanced <https://github.com/roman-ryzenadvanced>",
|
||||
"author": "Roman | RyzenAdvanced <https://github.rommark.dev/admin>",
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git"
|
||||
"url": "https://github.rommark.dev/admin/PromptArch.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer/issues"
|
||||
"url": "https://github.rommark.dev/admin/PromptArch/issues"
|
||||
},
|
||||
"homepage": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer#readme"
|
||||
}
|
||||
"homepage": "https://github.rommark.dev/admin/PromptArch"
|
||||
}
|
||||
244
types/index.ts
244
types/index.ts
@@ -1,4 +1,4 @@
|
||||
export type ModelProvider = "qwen" | "ollama" | "zai";
|
||||
export type ModelProvider = "qwen" | "ollama" | "zai" | "openrouter";
|
||||
|
||||
export interface ModelConfig {
|
||||
provider: ModelProvider;
|
||||
@@ -91,3 +91,245 @@ export interface ChatMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Slide {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
htmlContent: string;
|
||||
notes?: string;
|
||||
layout: "title" | "content" | "two-column" | "image-left" | "image-right" | "quote" | "statistics" | "timeline" | "comparison";
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface SlidesPresentation {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
author?: string;
|
||||
organization?: string;
|
||||
theme: "corporate" | "modern" | "minimal" | "dark" | "vibrant" | "gradient";
|
||||
language: string;
|
||||
slides: Slide[];
|
||||
rawContent: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface GoogleAdsKeyword {
|
||||
keyword: string;
|
||||
type: "primary" | "long-tail" | "negative";
|
||||
searchVolume?: number;
|
||||
competition: "low" | "medium" | "high";
|
||||
difficultyScore?: number;
|
||||
relevanceScore?: number;
|
||||
cpc?: string;
|
||||
}
|
||||
|
||||
export interface GoogleAdCopy {
|
||||
id: string;
|
||||
campaignType: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||
headlines: string[];
|
||||
descriptions: string[];
|
||||
callToAction: string;
|
||||
displayUrl?: string;
|
||||
finalUrl?: string;
|
||||
mobileOptimized: boolean;
|
||||
}
|
||||
|
||||
export interface GoogleAdGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
theme: string;
|
||||
keywords: string[];
|
||||
ads: GoogleAdCopy[];
|
||||
biddingStrategy?: string;
|
||||
}
|
||||
|
||||
export interface GoogleAdsCampaign {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||
budget: {
|
||||
daily?: number;
|
||||
monthly?: number;
|
||||
currency: string;
|
||||
};
|
||||
targeting: {
|
||||
locations?: string[];
|
||||
demographics?: {
|
||||
age?: string[];
|
||||
gender?: string[];
|
||||
interests?: string[];
|
||||
};
|
||||
devices?: {
|
||||
mobile?: string;
|
||||
desktop?: string;
|
||||
tablet?: string;
|
||||
};
|
||||
schedule?: string[];
|
||||
};
|
||||
adGroups: GoogleAdGroup[];
|
||||
}
|
||||
|
||||
export interface GoogleAdsResult {
|
||||
id: string;
|
||||
websiteUrl: string;
|
||||
productsServices: string[];
|
||||
generatedAt: Date;
|
||||
|
||||
// Keyword Research Package
|
||||
keywords: {
|
||||
primary: GoogleAdsKeyword[];
|
||||
longTail: GoogleAdsKeyword[];
|
||||
negative: GoogleAdsKeyword[];
|
||||
};
|
||||
|
||||
// Ad Copy Suite
|
||||
adCopies: GoogleAdCopy[];
|
||||
|
||||
// Campaign Structure
|
||||
campaigns: GoogleAdsCampaign[];
|
||||
|
||||
// Implementation Guidance
|
||||
implementation: {
|
||||
setupSteps: string[];
|
||||
qualityScoreTips: string[];
|
||||
trackingSetup: string[];
|
||||
optimizationTips: string[];
|
||||
};
|
||||
|
||||
// Performance Predictions
|
||||
predictions?: {
|
||||
estimatedClicks?: string;
|
||||
estimatedImpressions?: string;
|
||||
estimatedCtr?: string;
|
||||
estimatedConversions?: string;
|
||||
conversionRate?: string;
|
||||
avgCpc?: string;
|
||||
};
|
||||
historicalBenchmarks?: {
|
||||
industryAverageCtr?: string;
|
||||
industryAverageCpc?: string;
|
||||
seasonalTrends?: string;
|
||||
geographicInsights?: string;
|
||||
};
|
||||
|
||||
rawContent: string;
|
||||
}
|
||||
|
||||
export interface MagicWandStrategy {
|
||||
id: string;
|
||||
direction: string;
|
||||
rationale: string;
|
||||
targetAudience: string;
|
||||
targetingDetails?: {
|
||||
geography?: string;
|
||||
demographics?: string;
|
||||
behavior?: string;
|
||||
};
|
||||
competitiveAdvantage: string;
|
||||
messagingPillars?: string[];
|
||||
keyMessages: string[];
|
||||
adCopyGuide: {
|
||||
headlines: string[];
|
||||
descriptions: string[];
|
||||
keywords: string[];
|
||||
setupGuide: string;
|
||||
};
|
||||
recommendedChannels: string[];
|
||||
estimatedBudgetAllocation: {
|
||||
search?: number;
|
||||
display?: number;
|
||||
video?: number;
|
||||
social?: number;
|
||||
};
|
||||
expectedROI: string;
|
||||
riskLevel: "low" | "medium" | "high";
|
||||
timeToResults: string;
|
||||
successMetrics?: string[];
|
||||
}
|
||||
|
||||
export interface MagicWandResult {
|
||||
id: string;
|
||||
websiteUrl: string;
|
||||
product: string;
|
||||
budget: number;
|
||||
generatedAt: Date;
|
||||
|
||||
// Market Intelligence
|
||||
marketAnalysis: {
|
||||
industrySize: string;
|
||||
growthRate: string;
|
||||
topCompetitors: string[];
|
||||
marketTrends: string[];
|
||||
};
|
||||
|
||||
// Competitive Intelligence
|
||||
competitorInsights: {
|
||||
competitor: string;
|
||||
website?: string;
|
||||
estimatedSpend?: string;
|
||||
targetAudience?: string;
|
||||
strengths: string[];
|
||||
weaknesses: string[];
|
||||
adStrategy: string;
|
||||
topKeywords?: string[];
|
||||
adCopyExamples?: { headline: string; description: string }[];
|
||||
}[];
|
||||
|
||||
// Strategic Directions
|
||||
strategies: MagicWandStrategy[];
|
||||
|
||||
rawContent: string;
|
||||
}
|
||||
|
||||
export interface MarketResearchResult {
|
||||
id: string;
|
||||
websiteUrl: string;
|
||||
additionalUrls: string[];
|
||||
competitors: string[];
|
||||
productMapping: {
|
||||
productName: string;
|
||||
features: string[];
|
||||
pricePoint?: string;
|
||||
}[];
|
||||
generatedAt: Date;
|
||||
|
||||
executiveSummary: string;
|
||||
priceComparisonMatrix: {
|
||||
product: string;
|
||||
userPrice: string;
|
||||
competitorPrices: { competitor: string; price: string; url?: string }[];
|
||||
}[];
|
||||
featureComparisonTable: {
|
||||
feature: string;
|
||||
userStatus: boolean | string;
|
||||
competitorStatus: { competitor: string; status: boolean | string }[];
|
||||
}[];
|
||||
marketPositioning: {
|
||||
landscape: string;
|
||||
segmentation: string;
|
||||
};
|
||||
competitiveAnalysis: {
|
||||
advantages: string[];
|
||||
disadvantages: string[];
|
||||
};
|
||||
recommendations: string[];
|
||||
methodology: string;
|
||||
rawContent: string;
|
||||
}
|
||||
|
||||
export interface AIAssistMessage {
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
agent?: string;
|
||||
preview?: {
|
||||
type: string;
|
||||
data: string;
|
||||
language?: string;
|
||||
};
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export type AppView = "prompt-enhancer" | "prd-generator" | "action-plan" | "slides-gen" | "google-ads" | "ux-designer" | "market-research" | "ai-assist" | "settings" | "history";
|
||||
|
||||
Reference in New Issue
Block a user