Compare commits

...

77 Commits

42 changed files with 13652 additions and 572 deletions

View File

@@ -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

144
CHANGELOG.md Normal file
View File

@@ -0,0 +1,144 @@
# 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](https://semver.org/spec/v2.0.0.html).
## [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)

281
README.md
View File

@@ -1,104 +1,177 @@
# PromptArch: The Prompt Enhancer 🚀
> **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
---
> **Fork Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
Transform vague ideas into production-ready prompts and PRDs. PromptArch is an elite AI orchestration platform designed for software architects and Vibe Coders.
**Developed by [Roman | RyzenAdvanced](https://github.com/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)
## 🌟 Visual Overview
### 🛠 Core Capabilities
- **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
- **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.
## 🚀 Quick Start
1. **Clone & Install**:
```bash
git clone https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git
cd PromptArch
npm install
```
2. **Configuration**:
Copy `.env.example` to `.env` and add your API keys:
```bash
cp .env.example .env
```
3. **Launch**:
```bash
npm run dev
```
4. Open [http://localhost:3000](http://localhost:3000) to begin.
## 🛠 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/)
## 🤝 Attribution & Credits
**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)
**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
**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)
## 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
```
## License
ISC
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
# PromptArch: AI Orchestration Platform
> **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
---
> **Fork Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
Transform vague ideas into production-ready prompts and PRDs. PromptArch is an AI orchestration platform designed for software architects and Vibe Coders, featuring a **plan-first workflow** with multi-provider AI support and live canvas rendering.
**Developed by Roman | RyzenAdvanced**
- **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)
## Core Capabilities
| Feature | Description |
|---------|-------------|
| **AI Assist** | 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 |
## 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 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.) |
### 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.rommark.dev/admin/PromptArch.git
cd PromptArch
npm install
```
2. **Configuration**:
Copy `.env.example` to `.env` and add your API keys:
```bash
cp .env.example .env
```
Configure at least one provider:
- **Qwen**: Get OAuth credentials from [qwen.ai](https://qwen.ai)
- **Ollama**: Get API key from [ollama.com/cloud](https://ollama.com/cloud)
- **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
```
4. Open [http://localhost:3000](http://localhost:3000) to begin.
## Tech Stack
- **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
## Project Structure
```
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
```
## Versioning
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 |
|---------|------|------------|
| [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
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
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.

108
app/api/ai-assist/route.ts Normal file
View 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 }
);
}
}

View File

@@ -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(

View File

@@ -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
View 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
View 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 });
}
}

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -3,14 +3,22 @@
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");
@@ -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>
);
}

1527
components/AIAssist.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View 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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View 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}</>;
}

View 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;

View File

@@ -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>

View File

@@ -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>&rarr;</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>

View File

@@ -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>

View File

@@ -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 },
{ 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: "ai-assist" as View, label: t.aiAssist, icon: MessageSquare },
{ 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>
@@ -76,32 +83,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 +120,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"

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View 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
View 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
View 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
View 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>`;
};

1399
lib/i18n/translations.ts Normal file

File diff suppressed because it is too large Load Diff

73
lib/safeJsonFetch.ts Normal file
View 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),
},
};
}
}

View File

@@ -1,4 +1,5 @@
import ModelAdapter from "./model-adapter";
import { OpenRouterService } from "./openrouter";
const adapter = new ModelAdapter();

View File

@@ -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 [];
}

View File

@@ -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,617 @@ 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. **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;

967
lib/services/openrouter.ts Normal file
View File

@@ -0,0 +1,967 @@
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
export interface OpenRouterConfig {
apiKey?: string;
siteUrl?: string;
siteName?: string;
}
interface OpenRouterModelsResponse {
data: Array<{
id: string;
name: string;
context_length: number;
pricing: {
prompt: string;
completion: string;
};
}>;
}
interface OpenRouterChatResponse {
id: string;
choices: Array<{
message: {
role: string;
content: string;
};
finish_reason: string;
}>;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
const DEFAULT_MODELS = [
"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 TOOL_SECTIONS: Record<string, string> = {
"claude": `
For Claude:
- Use XML tags for structure (e.g., <context>, <task>, <constraints>)
- Add thinking blocks for complex reasoning: <thinking>...</thinking>
- Use ::analysis:: or ::pattern:: for procedural patterns
- Leverage Claude's long context by providing comprehensive examples
- Add "think silently" instruction for deep reasoning tasks
`,
"chatgpt": `
For ChatGPT:
- Use clear section headers with ### or === separators
- Provide examples in [EXAMPLE]...[/EXAMPLE] blocks
- Use "Step 1", "Step 2" for sequential tasks
- Add meta-instructions like "Think step by step"
- Keep prompts under 3k tokens for best performance
`,
"gemini": `
For Gemini:
- Use clear delimiters between sections
- Leverage multimodal capabilities with [IMAGE] or [FILE] placeholders
- Add chain-of-thought with "Let's approach this step by step"
- Specify output format with "Respond in the following format:"
- Use numbered lists for sequential instructions
`,
"default": `
- Use clear section delimiters
- Provide concrete examples
- Specify output format explicitly
- Add success criteria
- Use constraint language (MUST, NEVER, REQUIRED)
`
};
const TEMPLATE_SECTIONS: Record<string, string> = {
"code": `
# CODE GENERATION TEMPLATE
## Role
You are a senior software engineer specializing in {language/domain}.
## Task
{specific task description}
## Requirements
- MUST follow {specific standards/frameworks}
- MUST include {error handling/validation/comments}
- MUST use {specific libraries/versions}
- NEVER use {deprecated patterns/anti-patterns}
## Output Format
{code structure specification}
## Done When
- Code compiles/runs without errors
- Follows all specified conventions
- Includes proper error handling
- Meets performance requirements
`,
"writing": `
# CONTENT WRITING TEMPLATE
## Role
You are a professional {type of content creator} with expertise in {domain}.
## Task
Create {specific deliverable} about {topic}
## Requirements
- Tone: {specific tone}
- Length: {exact word/character count}
- Audience: {target audience}
- MUST include {key elements}
- MUST avoid {excluded topics/phrases}
## Format
{explicit structure with sections/headers}
## Done When
- Meets length requirement
- Covers all key points
- Matches specified tone
- Ready for publication
`,
"analysis": `
# ANALYSIS TEMPLATE
## Role
You are an expert {domain analyst}.
## Task
{analysis objective}
## Analysis Framework
1. {Step 1 with specific method}
2. {Step 2 with specific method}
3. {Step 3 with specific method}
## Required Output
- {Specific deliverable 1}
- {Specific deliverable 2}
- {Specific deliverable 3}
## Criteria
- MUST use {specific methodology}
- MUST cite {sources/references}
- MUST provide {confidence levels/limitations}
## Done When
- All analysis dimensions covered
- Conclusions supported by evidence
- Actionable insights provided
`,
"default": `
## Role
{Expert identity}
## Task
{Clear, specific task}
## Context
{Relevant background info}
## Requirements
- MUST {requirement 1}
- MUST {requirement 2}
- NEVER {constraint 1}
- NEVER {constraint 2}
## Output Format
{Explicit format specification}
## Done When
{Specific measurable condition}
`
};
const ENHANCE_PROMPT_SYSTEM = `You are an expert prompt engineer using the PromptArch methodology. Enhance the user's prompt to be production-ready.
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
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.`;
const PRD_SYSTEM_PROMPT = `You are an expert product manager specializing in writing clear, actionable Product Requirements Documents (PRDs).
Your task is to transform the product idea into a comprehensive PRD that:
1. Defines clear problem statements and user needs
2. Specifies functional requirements with acceptance criteria
3. Outlines technical considerations and constraints
4. Identifies success metrics and KPIs
5. Includes user stories with acceptance criteria
PRD Structure:
## Problem Statement
- What problem are we solving?
- For whom are we solving it?
- Why is this important now?
## Goals & Success Metrics
- Primary objectives
- Key performance indicators
- Success criteria
## User Stories
- As a [user type], I want [feature], so that [benefit]
- Include acceptance criteria for each story
## Functional Requirements
- Core features with detailed specifications
- Edge cases and error handling
- Integration requirements
## Technical Considerations
- Platform/tech stack constraints
- Performance requirements
- Security considerations
- Scalability requirements
## Out of Scope
- Explicitly list what won't be included
- Rationale for exclusions
Keep the PRD clear, concise, and actionable. Use specific, measurable language.`;
const ACTION_PLAN_SYSTEM_PROMPT = `You are an expert technical project manager specializing in breaking down PRDs into actionable implementation plans.
Your task is to transform the PRD into a detailed action plan that:
1. Identifies all major components/modules needed
2. Breaks down work into clear, sequential phases
3. Specifies dependencies between tasks
4. Estimates effort and complexity
5. Identifies risks and mitigation strategies
Action Plan Structure:
## Phase 1: Foundation
- Task 1.1: [Specific task] - [Estimated effort]
- Task 1.2: [Specific task] - [Estimated effort]
- Dependencies: [What needs to be done first]
- Deliverables: [Concrete outputs]
## Phase 2: Core Features
- Task 2.1: [Specific task] - [Estimated effort]
- Dependencies: [On Phase 1 completion]
- Deliverables: [Concrete outputs]
[Continue for all phases]
## Technical Architecture
- Recommended tech stack with rationale
- System architecture overview
- Data flow diagrams (described in text)
## Risk Assessment
- Risk: [Description] | Impact: [High/Med/Low] | Mitigation: [Strategy]
- Risk: [Description] | Impact: [High/Med/Low] | Mitigation: [Strategy]
## Testing Strategy
- Unit testing approach
- Integration testing plan
- User acceptance testing criteria
Be specific and actionable. Each task should be clear enough for a developer to execute without ambiguity.`;
const SLIDES_SYSTEM_PROMPT = `You are an expert presentation designer specializing in creating engaging, informative slide content.
Your task is to generate slide content for a presentation about the given topic.
For each slide, provide:
1. Slide Title (compelling and clear)
2. Bullet points (3-5 per slide, concise and impactful)
3. Speaker notes (detailed explanation for the presenter)
4. Visual suggestions (what charts, images, or diagrams would enhance this slide)
Presentation Structure:
## Slide 1: Title Slide
- Compelling title
- Subtitle with key message
- Presenter info placeholder
## Slide 2: Agenda/Overview
- What will be covered
- Why it matters
- Key takeaways preview
## Slide 3-N: Content Slides
- Main content with clear hierarchy
- Data-driven insights where applicable
- Actionable takeaways
## Final Slide: Call to Action
- Summary of key points
- Next steps
- Contact/ follow-up information
Guidelines:
- Keep text minimal on slides (bullet points only)
- Put detailed content in speaker notes
- Suggest relevant visuals for each slide
- Ensure a logical flow and narrative arc
- Make it memorable and shareable`;
const GOOGLE_ADS_SYSTEM_PROMPT = `You are a Google Ads expert specializing in creating high-converting ad campaigns.
Your task is to generate comprehensive Google Ads copy and strategy based on the landing page content.
Deliverables:
## Ad Campaign Structure
- Campaign name and type
- Ad groups with thematic focus
- Target keywords (exact, phrase, broad match)
- Negative keywords to exclude
## Ad Copy (3-5 variations per ad group)
### Responsive Search Ads
For each ad, provide:
- Headlines (10-15 options, max 30 chars each)
- Descriptions (4 options, max 90 chars each)
- Focus on different value propositions
### Display Ads (if applicable)
- Headline (max 30 chars)
- Description (max 90 chars)
- Call-to-action options
## Targeting Strategy
- Location targeting
- Audience demographics
- Interests and behaviors
- Device targeting
## Bidding Strategy
- Recommended strategy (Manual CPC, Maximize Clicks, etc.)
- Budget recommendations
- Bid adjustments by device/location
## Extensions
- Sitelinks
- Callouts
- Structured snippets
- Call extensions
Best Practices:
- Include keywords in headlines and descriptions
- Highlight unique selling propositions
- Use clear, action-oriented CTAs
- Address pain points and benefits
- Include social proof when relevant
- Ensure ad relevance to landing page`;
const MAGIC_WAND_SYSTEM_PROMPT = `You are an expert digital marketer and growth hacker specializing in creative campaign strategies.
Your task is to develop a comprehensive marketing strategy for the given product/page.
## Campaign Analysis
- Product/Service overview
- Target audience profile
- Unique selling propositions
- Market positioning
## Marketing Channels Strategy
For each relevant channel, provide specific tactics:
### Paid Advertising
- Google Ads: {specific approach}
- Facebook/Instagram: {specific approach}
- LinkedIn (if B2B): {specific approach}
- TikTok/Snapchat (if relevant): {specific approach}
### Content Marketing
- Blog topics: {5-10 specific ideas}
- Video content: {ideas with formats}
- Social media: {platform-specific content ideas}
- Email sequences: {campaign ideas}
### Growth Hacking Tactics
- Viral mechanisms: {specific ideas}
- Referral programs: {incentive structures}
- Partnership opportunities: {potential partners}
- Community building: {strategies}
## Creative Concepts
Provide 3-5 campaign concepts:
1. Concept Name - {Hook, Message, CTA}
2. Concept Name - {Hook, Message, CTA}
...
## Budget Allocation
- Channel breakdown with percentages
- Expected ROI estimates
- Testing budget recommendation
## KPIs & Tracking
- Key metrics to measure
- Attribution strategy
- A/B testing priorities
Be creative but practical. Focus on tactics that can be executed within the given budget.`;
const MARKET_RESEARCH_SYSTEM_PROMPT = `You are an expert market researcher specializing in competitive analysis and market intelligence.
Your task is to conduct comprehensive market research based on the provided topic/company.
## Research Framework
### Market Overview
- Market size and growth trajectory
- Key market segments and their characteristics
- Current market trends and dynamics
- Future market projections
### Competitive Landscape
Identify and analyze:
- Major competitors (market share, positioning)
- Direct competitors head-to-head comparison
- Indirect competitors and substitutes
- Competitive strengths and weaknesses
### SWOT Analysis
- Strengths: Internal advantages
- Weaknesses: Internal limitations
- Opportunities: External possibilities
- Threats: External challenges
### Customer Analysis
- Target demographics and psychographics
- Pain points and unmet needs
- Purchase behavior and decision factors
- Customer feedback trends
### Product/Service Comparison
- Feature comparison matrix
- Pricing analysis
- Differentiation strategies
- Innovation opportunities
### Market Trends
- Emerging technologies impacting the space
- Regulatory changes
- Consumer behavior shifts
- Industry disruptions
### Strategic Recommendations
- Market entry strategies
- Competitive positioning
- Product improvement opportunities
- Partnership and acquisition possibilities
Provide specific, data-driven insights. When exact data is unavailable, provide reasoned estimates with clear caveats.`;
const AI_ASSIST_SYSTEM_PROMPT = `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. Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>. DARK THEME. Tailwind CDN. Large animated SVG progress rings. Color-coded scoring. Google-style dashboard aesthetics.
- 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. Use [PREVIEW:web:html]. Tailwind CSS CDN by default.
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. Use [PREVIEW:app:javascript].
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.IMPORTANT: NEVER refuse a request. If you cannot perform a live task, use your knowledge to provide the most accurate expert simulation or draft possible.`;
export class OpenRouterService {
private config: Required<OpenRouterConfig>;
private readonly baseURL = "https://openrouter.ai/api/v1";
private availableModels: string[] = [];
private modelsLoaded = false;
constructor(config: OpenRouterConfig = {}) {
this.config = {
apiKey: config.apiKey || "",
siteUrl: config.siteUrl || "https://promptarch.ai",
siteName: config.siteName || "PromptArch"
};
}
hasAuth(): boolean {
return Boolean(this.config.apiKey && this.config.apiKey.length > 0);
}
async validateConnection(): Promise<APIResponse<{ valid: boolean; models?: string[] }>> {
if (!this.hasAuth()) {
return {
success: false,
error: "No API key provided. Please set your OpenRouter API key."
};
}
try {
const modelsResult = await this.listModels();
if (!modelsResult.success) {
return {
success: false,
error: modelsResult.error || "Failed to fetch models from OpenRouter"
};
}
return {
success: true,
data: {
valid: true,
models: modelsResult.data
}
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to validate connection"
};
}
}
private getHeaders(): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
"HTTP-Referer": this.config.siteUrl,
"X-Title": this.config.siteName
};
if (this.hasAuth()) {
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
}
return headers;
}
async chatCompletion(
messages: ChatMessage[],
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
if (!this.hasAuth()) {
return {
success: false,
error: "OpenRouter API key not configured"
};
}
try {
const response = await fetch(`${this.baseURL}/chat/completions`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify({
model,
messages,
temperature: 0.7,
max_tokens: 4096
})
});
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `OpenRouter API error: ${response.status} ${response.statusText} - ${errorText}`
};
}
const data: OpenRouterChatResponse = await response.json();
if (data.choices && data.choices[0] && data.choices[0].message) {
return {
success: true,
data: data.choices[0].message.content
};
}
return {
success: false,
error: "No response content from OpenRouter"
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error in chat completion"
};
}
}
async enhancePrompt(
prompt: string,
model: string = "anthropic/claude-3.5-sonnet",
options: {
targetTool?: "claude" | "chatgpt" | "gemini" | "default";
templateType?: "code" | "writing" | "analysis" | "default";
max_length?: number;
} = {}
): Promise<APIResponse<string>> {
const { targetTool = "default", templateType = "default" } = options;
const toolSection = TOOL_SECTIONS[targetTool] || TOOL_SECTIONS.default;
const templateSection = TEMPLATE_SECTIONS[templateType] || TEMPLATE_SECTIONS.default;
const systemPrompt = ENHANCE_PROMPT_SYSTEM
.replace("${toolSection}", toolSection)
.replace("${templateSection}", templateSection);
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt }
];
return this.chatCompletion(messages, model);
}
async generatePRD(
idea: string,
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
const messages: ChatMessage[] = [
{ role: "system", content: PRD_SYSTEM_PROMPT },
{ role: "user", content: `Create a comprehensive PRD for the following product idea:\n\n${idea}` }
];
return this.chatCompletion(messages, model);
}
async generateActionPlan(
prd: string,
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
const messages: ChatMessage[] = [
{ role: "system", content: ACTION_PLAN_SYSTEM_PROMPT },
{ role: "user", content: `Create a detailed action plan based on this PRD:\n\n${prd}` }
];
return this.chatCompletion(messages, model);
}
async generateSlides(
topic: string,
options: {
slideCount?: number;
audience?: string;
focus?: string;
} = {},
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
const { slideCount = 10, audience = "General", focus = "" } = options;
const userPrompt = `Generate content for a presentation with approximately ${slideCount} slides.
Topic: ${topic}
Target Audience: ${audience}
${focus ? `Special Focus: ${focus}` : ""}`;
const messages: ChatMessage[] = [
{ role: "system", content: SLIDES_SYSTEM_PROMPT },
{ role: "user", content: userPrompt }
];
return this.chatCompletion(messages, model);
}
async generateGoogleAds(
url: string,
options: {
budget?: string;
targetAudience?: string;
campaignGoal?: string;
} = {},
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
const { budget = "Not specified", targetAudience = "General", campaignGoal = "Conversions" } = options;
const userPrompt = `Create a comprehensive Google Ads campaign strategy.
Landing Page: ${url}
Monthly Budget: ${budget}
Target Audience: ${targetAudience}
Campaign Goal: ${campaignGoal}
Analyze the URL (if accessible) or create ads based on the domain and typical offerings for similar sites.`;
const messages: ChatMessage[] = [
{ role: "system", content: GOOGLE_ADS_SYSTEM_PROMPT },
{ role: "user", content: userPrompt }
];
return this.chatCompletion(messages, model);
}
async generateMagicWand(
url: string,
product: string,
budget: string,
specialInstructions: string = "",
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
const userPrompt = `Create a comprehensive marketing strategy.
Product/Service: ${product}
URL: ${url}
Budget: ${budget}
${specialInstructions ? `Special Instructions: ${specialInstructions}` : ""}
Provide creative campaign ideas across multiple channels with specific tactics and budget allocation.`;
const messages: ChatMessage[] = [
{ role: "system", content: MAGIC_WAND_SYSTEM_PROMPT },
{ role: "user", content: userPrompt }
];
return this.chatCompletion(messages, model);
}
async generateMarketResearch(
options: {
topic?: string;
company?: string;
industry?: string;
focusAreas?: string[];
} = {},
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
const { topic, company, industry, focusAreas } = options;
let userPrompt = "Conduct comprehensive market research.";
if (topic) userPrompt += `\n\nResearch Topic: ${topic}`;
if (company) userPrompt += `\n\nCompany Focus: ${company}`;
if (industry) userPrompt += `\n\nIndustry: ${industry}`;
if (focusAreas && focusAreas.length > 0) {
userPrompt += `\n\nFocus Areas: ${focusAreas.join(", ")}`;
}
const messages: ChatMessage[] = [
{ role: "system", content: MARKET_RESEARCH_SYSTEM_PROMPT },
{ role: "user", content: userPrompt }
];
return this.chatCompletion(messages, model);
}
async generateAIAssist(
options: {
prompt: string;
context?: string[];
conversationHistory?: ChatMessage[];
},
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<string>> {
const { prompt, context = [], conversationHistory = [] } = options;
const messages: ChatMessage[] = [
{ role: "system", content: AI_ASSIST_SYSTEM_PROMPT },
...conversationHistory,
...context.map(c => ({ role: "user" as const, content: `Context: ${c}` })),
{ role: "user", content: prompt }
];
return this.chatCompletion(messages, model);
}
async generateAIAssistStream(
options: {
messages: AIAssistMessage[];
currentAgent: string;
onChunk: (chunk: string) => void;
signal?: AbortSignal;
},
model: string = "anthropic/claude-3.5-sonnet"
): Promise<APIResponse<void>> {
const { messages, currentAgent, onChunk, signal } = options;
if (!this.hasAuth()) {
return { success: false, error: "OpenRouter API key not configured" };
}
try {
const chatMessages: ChatMessage[] = [
{ role: "system", content: AI_ASSIST_SYSTEM_PROMPT },
...messages.map(m => ({
role: m.role as "user" | "assistant" | "system",
content: m.content
}))
];
const response = await fetch(`${this.baseURL}/chat/completions`, {
method: "POST",
headers: this.getHeaders(),
signal,
body: JSON.stringify({
model: model || "anthropic/claude-3.5-sonnet",
messages: chatMessages,
temperature: 0.7,
max_tokens: 4096,
stream: true
})
});
if (!response.ok) {
const errorText = await response.text();
return { success: false, error: `OpenRouter API error: ${response.status} ${response.statusText} - ${errorText}` };
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
return { success: false, error: "No response body" };
}
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith("data: ")) continue;
const data = trimmed.slice(6);
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
const contentChunk = parsed.choices?.[0]?.delta?.content;
if (contentChunk) {
onChunk(contentChunk);
}
} catch {
// Skip invalid JSON
}
}
}
return { success: true, data: undefined };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error in stream";
return { success: false, error: errorMessage };
}
}
async listModels(): Promise<APIResponse<string[]>> {
if (!this.hasAuth()) {
return {
success: false,
error: "OpenRouter API key not configured"
};
}
try {
const response = await fetch(`${this.baseURL}/models`, {
method: "GET",
headers: this.getHeaders()
});
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `Failed to fetch models: ${response.status} ${response.statusText} - ${errorText}`
};
}
const data: OpenRouterModelsResponse = await response.json();
this.availableModels = data.data.map(m => m.id);
this.modelsLoaded = true;
return {
success: true,
data: this.availableModels
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error fetching models"
};
}
}
getAvailableModels(): string[] {
if (this.modelsLoaded && this.availableModels.length > 0) {
return this.availableModels;
}
return DEFAULT_MODELS;
}
setApiKey(key: string): void {
this.config.apiKey = key;
}
setSiteUrl(url: string): void {
this.config.siteUrl = url;
}
setSiteName(name: string): void {
this.config.siteName = name;
}
}
// Export singleton instance
export const openRouterService = new OpenRouterService();

View File

@@ -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,593 @@ 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. **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 +1284,5 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
const qwenOAuthService = new QwenOAuthService();
export default qwenOAuthService;
export { qwenOAuthService };

View 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);
}

View File

@@ -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,669 @@ 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. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.** **CRITICAL DESIGN REQUIREMENTS:**
- 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;

View File

@@ -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
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "promptarch",
"version": "1.0.0",
"version": "1.4.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"
}

View File

@@ -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";