Compare commits

...

84 Commits

45 changed files with 10781 additions and 834 deletions

15
LICENSE Normal file
View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2024 Roman | RyzenAdvanced
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,14 +1,19 @@
# PromptArch: The Prompt Enhancer 🚀 # PromptArch: The Prompt Enhancer 🚀
> **Development Note**: This entire platform was developed exclusively using the [TRAE.AI IDE](https://trae.ai) powered by the elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW). > **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).** > **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
--- ---
> **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. > **Fork Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
Transform vague ideas into production-ready prompts and PRDs. PromptArch is an elite AI orchestration platform designed for software architects and Vibe Coders. Transform vague ideas into production-ready prompts and PRDs. PromptArch is an 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 ## 🌟 Visual Overview
### 🛠 Core Capabilities ### 🛠 Core Capabilities
@@ -56,21 +61,20 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an e
- **Components**: [shadcn/ui](https://ui.shadcn.com/) - **Components**: [shadcn/ui](https://ui.shadcn.com/)
- **Icons**: [Lucide React](https://lucide.dev/) - **Icons**: [Lucide React](https://lucide.dev/)
## 🤝 Attribution ## 🤝 Attribution & Credits
This project is a visual and architectural evolution of the [Clavix](https://github.com/ClavixDev/Clavix) framework. While Clavix focuses on agentic-first Markdown templates, PromptArch provides a centralized web interface to execute these workflows with advanced model orchestration. **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)
Developed by **Roman | RyzenAdvanced** **Forked from**: [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix)
- GitHub: [roman-ryzenadvanced](https://github.com/roman-ryzenadvanced) - This project is a visual and architectural evolution of the Clavix framework
- Telegram: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem) - 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% Developed using GLM 4.7 model on TRAE.AI IDE.* - 100% AI-assisted development using TRAE.AI's advanced coding capabilities
- **Styling**: TailwindCSS - Learn more about the architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW)
- **UI Components**: shadcn/ui + Radix UI
- **State Management**: Zustand
- **Forms**: React Hook Form + Zod
- **Icons**: Lucide React
## Development ## Development

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), body: JSON.stringify(body),
}); });
const payload = await response.text();
if (!response.ok) { if (!response.ok) {
const payload = await response.text();
return NextResponse.json( return NextResponse.json(
{ error: "Ollama chat request failed", details: payload }, { error: "Ollama chat request failed", details: payload },
{ status: response.status } { 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) { } catch (error) {
console.error("Ollama chat proxy failed", error); console.error("Ollama chat proxy failed", error);
return NextResponse.json( return NextResponse.json(

View File

@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { createQwenHeaders } from "../constants";
const DEFAULT_QWEN_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
function normalizeEndpoint(raw?: string | null): string {
const trimmed = (raw || "").trim();
if (!trimmed) {
return DEFAULT_QWEN_ENDPOINT;
}
if (trimmed.endsWith("/chat/completions")) {
return trimmed;
}
const cleaned = trimmed.replace(/\/$/, "");
return `${cleaned}/chat/completions`;
}
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const { endpoint, model, messages, stream } = body || {};
const authorization = request.headers.get("authorization") || body?.authorization;
if (!authorization) {
return NextResponse.json(
{ error: "Authorization header required" },
{ status: 401 }
);
}
const url = normalizeEndpoint(endpoint);
const response = await fetch(url, {
method: "POST",
headers: {
...createQwenHeaders("application/json"),
Authorization: authorization,
},
body: JSON.stringify({
model,
messages,
stream,
}),
});
if (!response.ok) {
const payload = await response.text();
return NextResponse.json(
{ error: payload || response.statusText || "Qwen chat failed" },
{ status: response.status }
);
}
// 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" },
{ status: 500 }
);
}
}

View File

@@ -4,3 +4,19 @@ export const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/t
export const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56"; export const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
export const QWEN_OAUTH_SCOPE = "openid profile email model.completion"; export const QWEN_OAUTH_SCOPE = "openid profile email model.completion";
export const QWEN_OAUTH_DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; export const QWEN_OAUTH_DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
export const QWEN_USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
export function createQwenHeaders(contentType?: string) {
const headers: Record<string, string> = {
Accept: "application/json",
"User-Agent": QWEN_USER_AGENT,
};
if (contentType) {
headers["Content-Type"] = contentType;
}
return headers;
}

View File

@@ -3,6 +3,7 @@ import {
QWEN_OAUTH_CLIENT_ID, QWEN_OAUTH_CLIENT_ID,
QWEN_OAUTH_DEVICE_CODE_ENDPOINT, QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
QWEN_OAUTH_SCOPE, QWEN_OAUTH_SCOPE,
createQwenHeaders,
} from "../../constants"; } from "../../constants";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
@@ -17,12 +18,11 @@ export async function POST(request: NextRequest) {
); );
} }
console.log(`[Qwen Device Auth] Calling ${QWEN_OAUTH_DEVICE_CODE_ENDPOINT}...`);
const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, { const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: createQwenHeaders("application/x-www-form-urlencoded"),
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({ body: new URLSearchParams({
client_id: QWEN_OAUTH_CLIENT_ID, client_id: QWEN_OAUTH_CLIENT_ID,
scope: QWEN_OAUTH_SCOPE, scope: QWEN_OAUTH_SCOPE,
@@ -32,18 +32,25 @@ export async function POST(request: NextRequest) {
}); });
const payload = await response.text(); const payload = await response.text();
if (!response.ok) { console.log(`[Qwen Device Auth] Response status: ${response.status}`);
return NextResponse.json(
{ error: "Device authorization failed", details: payload }, let data;
{ status: response.status } try {
); data = JSON.parse(payload);
} catch {
console.error(`[Qwen Device Auth] Failed to parse response: ${payload}`);
data = { error: payload || "Unknown error from Qwen" };
} }
return NextResponse.json(JSON.parse(payload)); if (!response.ok) {
console.warn(`[Qwen Device Auth] Error response:`, data);
}
return NextResponse.json(data, { status: response.status });
} catch (error) { } catch (error) {
console.error("Qwen device authorization failed", error); console.error("Qwen device authorization failed", error);
return NextResponse.json( return NextResponse.json(
{ error: "Device authorization failed" }, { error: "internal_server_error", message: error instanceof Error ? error.message : "Device authorization failed" },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -1,5 +1,9 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { QWEN_OAUTH_CLIENT_ID, QWEN_OAUTH_TOKEN_ENDPOINT } from "../../constants"; import {
QWEN_OAUTH_CLIENT_ID,
QWEN_OAUTH_TOKEN_ENDPOINT,
createQwenHeaders,
} from "../../constants";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -15,10 +19,7 @@ export async function POST(request: NextRequest) {
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, { const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: createQwenHeaders("application/x-www-form-urlencoded"),
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token, refresh_token,
@@ -27,18 +28,18 @@ export async function POST(request: NextRequest) {
}); });
const payload = await response.text(); const payload = await response.text();
if (!response.ok) { let data;
return NextResponse.json( try {
{ error: "Token refresh failed", details: payload }, data = JSON.parse(payload);
{ status: response.status } } catch {
); data = { error: payload || "Unknown error from Qwen" };
} }
return NextResponse.json(JSON.parse(payload)); return NextResponse.json(data, { status: response.status });
} catch (error) { } catch (error) {
console.error("Qwen token refresh failed", error); console.error("Qwen token refresh failed", error);
return NextResponse.json( return NextResponse.json(
{ error: "Token refresh failed" }, { error: "internal_server_error", message: error instanceof Error ? error.message : "Token refresh failed" },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -3,6 +3,7 @@ import {
QWEN_OAUTH_CLIENT_ID, QWEN_OAUTH_CLIENT_ID,
QWEN_OAUTH_DEVICE_GRANT_TYPE, QWEN_OAUTH_DEVICE_GRANT_TYPE,
QWEN_OAUTH_TOKEN_ENDPOINT, QWEN_OAUTH_TOKEN_ENDPOINT,
createQwenHeaders,
} from "../../constants"; } from "../../constants";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
@@ -17,12 +18,11 @@ export async function POST(request: NextRequest) {
); );
} }
console.log(`[Qwen Token Poll] Calling ${QWEN_OAUTH_TOKEN_ENDPOINT} for device_code: ${device_code.slice(0, 8)}...`);
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, { const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: createQwenHeaders("application/x-www-form-urlencoded"),
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: QWEN_OAUTH_DEVICE_GRANT_TYPE, grant_type: QWEN_OAUTH_DEVICE_GRANT_TYPE,
client_id: QWEN_OAUTH_CLIENT_ID, client_id: QWEN_OAUTH_CLIENT_ID,
@@ -32,18 +32,25 @@ export async function POST(request: NextRequest) {
}); });
const payload = await response.text(); const payload = await response.text();
if (!response.ok) { console.log(`[Qwen Token Poll] Response status: ${response.status}`);
return NextResponse.json(
{ error: "Token poll failed", details: payload }, let data;
{ status: response.status } try {
); data = JSON.parse(payload);
} catch {
console.error(`[Qwen Token Poll] Failed to parse response: ${payload}`);
data = { error: payload || "Unknown error from Qwen" };
} }
return NextResponse.json(JSON.parse(payload)); if (data.error && data.error !== "authorization_pending") {
console.warn(`[Qwen Token Poll] Error in response:`, data);
}
return NextResponse.json(data, { status: response.status });
} catch (error) { } catch (error) {
console.error("Qwen token poll failed", error); console.error("Qwen token poll failed", error);
return NextResponse.json( return NextResponse.json(
{ error: "Token poll failed" }, { error: "internal_server_error", message: error instanceof Error ? error.message : "Token poll failed" },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { QWEN_OAUTH_BASE_URL } from "../constants"; import { QWEN_OAUTH_BASE_URL, createQwenHeaders } from "../constants";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@@ -14,16 +14,20 @@ export async function GET(request: NextRequest) {
const userResponse = await fetch(`${QWEN_OAUTH_BASE_URL}/api/v1/user`, { const userResponse = await fetch(`${QWEN_OAUTH_BASE_URL}/api/v1/user`, {
headers: { headers: {
...createQwenHeaders(),
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}); });
if (!userResponse.ok) { if (!userResponse.ok) {
const errorText = await userResponse.text(); const errorText = await userResponse.text();
return NextResponse.json( let errorData;
{ error: "Failed to fetch user info", details: errorText }, try {
{ status: userResponse.status } errorData = JSON.parse(errorText);
); } catch {
errorData = { error: errorText || "Failed to fetch user info" };
}
return NextResponse.json(errorData, { status: userResponse.status });
} }
const userData = await userResponse.json(); const userData = await userResponse.json();
@@ -31,7 +35,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
console.error("Qwen user info failed", error); console.error("Qwen user info failed", error);
return NextResponse.json( return NextResponse.json(
{ error: "Failed to fetch user info" }, { error: "internal_server_error", message: error instanceof Error ? error.message : "Failed to fetch user info" },
{ status: 500 } { 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,7 +63,71 @@
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @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"] {
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

@@ -1,14 +1,20 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Roboto } from "next/font/google";
import "./globals.css"; import "./globals.css";
const inter = Inter({ subsets: ["latin"] }); const roboto = Roboto({
subsets: ["latin"],
weight: ["300", "400", "500", "700"]
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "PromptArch - AI Prompt Engineering Platform", title: "PromptArch - AI Prompt Engineering Platform",
description: "Transform vague ideas into production-ready prompts and PRDs", description: "Transform vague ideas into production-ready prompts and PRDs",
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
}; };
import LocaleProvider from "@/components/LocaleProvider";
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
@@ -16,7 +22,11 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body className={inter.className}>{children}</body> <body className={roboto.className}>
<LocaleProvider>
{children}
</LocaleProvider>
</body>
</html> </html>
); );
} }

View File

@@ -1,18 +1,32 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
import type { View } from "@/components/Sidebar"; import type { View } from "@/components/Sidebar";
import PromptEnhancer from "@/components/PromptEnhancer"; import dynamic from 'next/dynamic';
import PRDGenerator from "@/components/PRDGenerator"; import modelAdapter from "@/lib/services/adapter-instance";
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
import UXDesignerPrompt from "@/components/UXDesignerPrompt"; // Dynamic imports to prevent hydration mismatches
import HistoryPanel from "@/components/HistoryPanel"; // ensuring hydration match
import SettingsPanel from "@/components/SettingsPanel"; 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() { export default function Home() {
const [currentView, setCurrentView] = useState<View>("enhance"); const [currentView, setCurrentView] = useState<View>("enhance");
useEffect(() => {
console.log("[Home] Initializing Qwen OAuth service on client...");
modelAdapter["qwenService"]["initialize"]?.();
}, []);
const renderContent = () => { const renderContent = () => {
switch (currentView) { switch (currentView) {
case "enhance": case "enhance":
@@ -23,6 +37,14 @@ export default function Home() {
return <ActionPlanGenerator />; return <ActionPlanGenerator />;
case "uxdesigner": case "uxdesigner":
return <UXDesignerPrompt />; return <UXDesignerPrompt />;
case "slides":
return <SlidesGenerator />;
case "googleads":
return <GoogleAdsGenerator />;
case "market-research":
return <MarketResearcher />;
case "ai-assist":
return <AIAssist />;
case "history": case "history":
return <HistoryPanel />; return <HistoryPanel />;
case "settings": case "settings":
@@ -35,7 +57,7 @@ export default function Home() {
return ( return (
<div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<Sidebar currentView={currentView} onViewChange={setCurrentView} /> <Sidebar currentView={currentView} onViewChange={setCurrentView} />
<main className="flex-1 overflow-auto p-8"> <main className="flex-1 overflow-auto pt-16 lg:pt-0 px-4 py-4 lg:p-8">
<div className="mx-auto max-w-7xl"> <div className="mx-auto max-w-7xl">
{renderContent()} {renderContent()}
</div> </div>
@@ -43,3 +65,4 @@ export default function Home() {
</div> </div>
); );
} }

1303
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 modelAdapter from "@/lib/services/adapter-instance";
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react"; import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function ActionPlanGenerator() { export default function ActionPlanGenerator() {
const { const {
language,
currentPrompt, currentPrompt,
actionPlan, actionPlan,
selectedProvider, selectedProvider,
@@ -28,6 +30,9 @@ export default function ActionPlanGenerator() {
setSelectedModel, setSelectedModel,
} = useStore(); } = useStore();
const t = translations[language].actionPlan;
const common = translations[language].common;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider]; const selectedModel = selectedModels[selectedProvider];
@@ -66,22 +71,28 @@ export default function ActionPlanGenerator() {
const handleGenerate = async () => { const handleGenerate = async () => {
if (!currentPrompt.trim()) { if (!currentPrompt.trim()) {
setError("Please enter PRD or project requirements"); setError(t.enterPrdError);
return; return;
} }
const apiKey = apiKeys[selectedProvider]; const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`${common.error}: ${common.configApiKey}`);
return; return;
} }
setProcessing(true); setProcessing(true);
setError(null); setError(null);
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try { try {
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel); const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
console.log("[ActionPlanGenerator] Generation result:", result);
if (result.success && result.data) { if (result.success && result.data) {
const newPlan = { const newPlan = {
id: Math.random().toString(36).substr(2, 9), id: Math.random().toString(36).substr(2, 9),
@@ -100,10 +111,12 @@ export default function ActionPlanGenerator() {
}; };
setActionPlan(newPlan); setActionPlan(newPlan);
} else { } else {
setError(result.error || "Failed to generate action plan"); console.error("[ActionPlanGenerator] Generation failed:", result.error);
setError(result.error || t.errorGenerate);
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "An error occurred"); console.error("[ActionPlanGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : t.errorGenerate);
} finally { } finally {
setProcessing(false); setProcessing(false);
} }
@@ -118,28 +131,28 @@ export default function ActionPlanGenerator() {
}; };
return ( return (
<div className="mx-auto grid max-w-7xl gap-6 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"> <Card className="h-fit">
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<ListTodo className="h-5 w-5" /> <ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
Action Plan Generator {t.title}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Convert PRD into actionable implementation plan {t.description}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">AI Provider</label> <label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button <Button
key={provider} key={provider}
variant={selectedProvider === provider ? "default" : "outline"} variant={selectedProvider === provider ? "default" : "outline"}
size="sm" size="sm"
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className="capitalize" className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
> >
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"} {provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button> </Button>
@@ -147,12 +160,12 @@ export default function ActionPlanGenerator() {
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">Model</label> <label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select <select
value={selectedModel} value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)} onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" 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) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
@@ -163,91 +176,90 @@ export default function ActionPlanGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">PRD / Requirements</label> <label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
<Textarea <Textarea
placeholder="Paste your PRD or project requirements here..." placeholder={t.placeholder}
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y" className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/> />
</div> </div>
{error && ( {error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error} {error}
{!apiKeys[selectedProvider] && ( {!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2"> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-xs">Configure API key in Settings</span> <span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div> </div>
)} )}
</div> </div>
)} )}
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full"> <Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
Generating Action Plan... {common.generating}
</> </>
) : ( ) : (
<> <>
<ListTodo className="mr-2 h-4 w-4" /> <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> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card className={cn(!actionPlan && "opacity-50")}> <Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
Action Plan {t.generatedTitle}
</span> </span>
{actionPlan && ( {actionPlan && (
<Button variant="ghost" size="icon" onClick={handleCopy}> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? ( {copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
)} )}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Task breakdown, frameworks, and architecture recommendations {t.generatedDesc}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{actionPlan ? ( {actionPlan ? (
<div className="space-y-4"> <div className="space-y-3 lg:space-y-4">
<div className="rounded-md border bg-primary/5 p-4"> <div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
<h4 className="mb-2 flex items-center gap-2 font-semibold"> <h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
<Clock className="h-4 w-4" /> <Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Implementation Roadmap {t.roadmap}
</h4> </h4>
<pre className="whitespace-pre-wrap text-sm">{actionPlan.rawContent}</pre> <pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
</div> </div>
<div className="rounded-md border bg-muted/30 p-4"> <div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
<h4 className="mb-2 flex items-center gap-2 font-semibold"> <h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Quick Notes {t.quickNotes}
</h4> </h4>
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground"> <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> {t.notes.map((note: string, i: number) => (
<li>Set up recommended framework architecture</li> <li key={i}>{note}</li>
<li>Follow best practices for security and performance</li> ))}
<li>Use specified deployment strategy</li>
</ul> </ul>
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground"> <div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
Action plan will appear here {t.emptyState}
</div> </div>
)} )}
</CardContent> </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,17 +4,19 @@ import useStore from "@/lib/store";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Clock, Trash2, RotateCcw } from "lucide-react"; import { Clock, Trash2, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils"; import { translations } from "@/lib/i18n/translations";
export default function HistoryPanel() { 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) => { const handleRestore = (prompt: string) => {
setCurrentPrompt(prompt); setCurrentPrompt(prompt);
}; };
const handleClear = () => { const handleClear = () => {
if (confirm("Are you sure you want to clear all history?")) { if (confirm(t.confirmClear)) {
clearHistory(); clearHistory();
} }
}; };
@@ -22,12 +24,12 @@ export default function HistoryPanel() {
if (history.length === 0) { if (history.length === 0) {
return ( return (
<Card> <Card>
<CardContent className="flex h-[400px] items-center justify-center"> <CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6 text-center">
<div className="text-center"> <div>
<Clock className="mx-auto h-12 w-12 text-muted-foreground/50" /> <Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
<p className="mt-4 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-2 text-sm text-muted-foreground"> <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> </p>
</div> </div>
</CardContent> </CardContent>
@@ -37,35 +39,37 @@ export default function HistoryPanel() {
return ( return (
<Card> <Card>
<CardHeader className="flex-row items-center justify-between"> <CardHeader className="flex-row items-center justify-between p-4 lg:p-6 text-start">
<div> <div>
<CardTitle>History</CardTitle> <CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
<CardDescription>{history.length} items</CardDescription> <CardDescription className="text-xs lg:text-sm">
{history.length} {t.items}
</CardDescription>
</div> </div>
<Button variant="outline" size="icon" onClick={handleClear}> <Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9" title={t.clear}>
<Trash2 className="h-4 w-4" /> <Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-2 lg:space-y-3 p-4 lg:p-6 pt-0 lg:pt-0">
{history.map((item) => ( {history.map((item) => (
<div <div
key={item.id} key={item.id}
className="rounded-md border bg-muted/30 p-4 transition-colors hover:bg-muted/50" className="rounded-md border bg-muted/30 p-3 lg:p-4 transition-colors hover:bg-muted/50"
> >
<div className="mb-2 flex items-center justify-between"> <div className="mb-1.5 lg:mb-2 flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground"> <span className="text-[10px] lg:text-xs text-muted-foreground truncate">
{new Date(item.timestamp).toLocaleString()} {new Date(item.timestamp).toLocaleString()}
</span> </span>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-6 w-6" className="h-6 w-6 flex-shrink-0"
onClick={() => handleRestore(item.prompt)} onClick={() => handleRestore(item.prompt)}
> >
<RotateCcw className="h-3 w-3" /> <RotateCcw className="h-3 w-3" />
</Button> </Button>
</div> </div>
<p className="line-clamp-3 text-sm">{item.prompt}</p> <p className="line-clamp-3 text-xs lg:text-sm">{item.prompt}</p>
</div> </div>
))} ))}
</CardContent> </CardContent>

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 modelAdapter from "@/lib/services/adapter-instance";
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react"; import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function PRDGenerator() { export default function PRDGenerator() {
const { const {
@@ -19,6 +20,7 @@ export default function PRDGenerator() {
apiKeys, apiKeys,
isProcessing, isProcessing,
error, error,
language,
setCurrentPrompt, setCurrentPrompt,
setSelectedProvider, setSelectedProvider,
setPRD, setPRD,
@@ -28,6 +30,9 @@ export default function PRDGenerator() {
setSelectedModel, setSelectedModel,
} = useStore(); } = useStore();
const t = translations[language].prdGenerator;
const common = translations[language].common;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [expandedSections, setExpandedSections] = useState<string[]>([]); const [expandedSections, setExpandedSections] = useState<string[]>([]);
@@ -73,22 +78,28 @@ export default function PRDGenerator() {
const handleGenerate = async () => { const handleGenerate = async () => {
if (!currentPrompt.trim()) { if (!currentPrompt.trim()) {
setError("Please enter an idea to generate PRD"); setError(t.enterIdeaError);
return; return;
} }
const apiKey = apiKeys[selectedProvider]; const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`${common.error}: ${common.configApiKey}`);
return; return;
} }
setProcessing(true); setProcessing(true);
setError(null); setError(null);
console.log("[PRDGenerator] Starting PRD generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try { try {
const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel); const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel);
console.log("[PRDGenerator] Generation result:", result);
if (result.success && result.data) { if (result.success && result.data) {
const newPRD = { const newPRD = {
id: Math.random().toString(36).substr(2, 9), id: Math.random().toString(36).substr(2, 9),
@@ -105,10 +116,12 @@ export default function PRDGenerator() {
}; };
setPRD(newPRD); setPRD(newPRD);
} else { } else {
setError(result.error || "Failed to generate PRD"); console.error("[PRDGenerator] Generation failed:", result.error);
setError(result.error || t.errorGenerate);
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "An error occurred"); console.error("[PRDGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : t.errorGenerate);
} finally { } finally {
setProcessing(false); setProcessing(false);
} }
@@ -123,37 +136,37 @@ export default function PRDGenerator() {
}; };
const sections = [ const sections = [
{ id: "overview", title: "Overview & Objectives" }, { id: "overview", title: t.sections.overview },
{ id: "personas", title: "User Personas & Use Cases" }, { id: "personas", title: t.sections.personas },
{ id: "functional", title: "Functional Requirements" }, { id: "functional", title: t.sections.functional },
{ id: "nonfunctional", title: "Non-functional Requirements" }, { id: "nonfunctional", title: t.sections.nonfunctional },
{ id: "architecture", title: "Technical Architecture" }, { id: "architecture", title: t.sections.architecture },
{ id: "metrics", title: "Success Metrics" }, { id: "metrics", title: t.sections.metrics },
]; ];
return ( return (
<div className="mx-auto grid max-w-7xl gap-6 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"> <Card className="h-fit">
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<FileText className="h-5 w-5" /> <FileText className="h-4 w-4 lg:h-5 lg:w-5" />
PRD Generator {t.title}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Generate comprehensive Product Requirements Document from your idea {t.description}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">AI Provider</label> <label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button <Button
key={provider} key={provider}
variant={selectedProvider === provider ? "default" : "outline"} variant={selectedProvider === provider ? "default" : "outline"}
size="sm" size="sm"
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className="capitalize" className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
> >
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"} {provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button> </Button>
@@ -161,12 +174,12 @@ export default function PRDGenerator() {
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">Model</label> <label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select <select
value={selectedModel} value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)} onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" 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) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
@@ -177,37 +190,36 @@ export default function PRDGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Your Idea</label>
<Textarea <Textarea
placeholder="e.g., A task management app with real-time collaboration features" placeholder={t.placeholder}
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y" className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/> />
</div> </div>
{error && ( {error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error} {error}
{!apiKeys[selectedProvider] && ( {!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2"> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-xs">Configure API key in Settings</span> <span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div> </div>
)} )}
</div> </div>
)} )}
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full"> <Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <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-2 h-4 w-4" /> <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> </Button>
@@ -215,53 +227,53 @@ export default function PRDGenerator() {
</Card> </Card>
<Card className={cn(!prd && "opacity-50")}> <Card className={cn(!prd && "opacity-50")}>
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
Generated PRD {t.generatedTitle}
</span> </span>
{prd && ( {prd && (
<Button variant="ghost" size="icon" onClick={handleCopy}> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? ( {copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
)} )}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Structured requirements document ready for development {t.generatedDesc}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{prd ? ( {prd ? (
<div className="space-y-3"> <div className="space-y-2 lg:space-y-3">
{sections.map((section) => ( {sections.map((section) => (
<div key={section.id} className="rounded-md border bg-muted/30"> <div key={section.id} className="rounded-md border bg-muted/30">
<button <button
onClick={() => toggleSection(section.id)} onClick={() => toggleSection(section.id)}
className="flex w-full items-center justify-between px-4 py-3 text-left font-medium transition-colors hover:bg-muted/50" className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
> >
<span>{section.title}</span> <span>{section.title}</span>
{expandedSections.includes(section.id) ? ( {expandedSections.includes(section.id) ? (
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</button> </button>
{expandedSections.includes(section.id) && ( {expandedSections.includes(section.id) && (
<div className="border-t bg-background px-4 py-3"> <div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3">
<pre className="whitespace-pre-wrap text-sm">{prd.overview}</pre> <pre className="whitespace-pre-wrap text-xs lg:text-sm">{prd.overview}</pre>
</div> </div>
)} )}
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground"> <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> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance"; import modelAdapter from "@/lib/services/adapter-instance";
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react"; import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function PromptEnhancer() { export default function PromptEnhancer() {
const { const {
language,
currentPrompt, currentPrompt,
enhancedPrompt, enhancedPrompt,
selectedProvider, selectedProvider,
@@ -28,6 +30,9 @@ export default function PromptEnhancer() {
setSelectedModel, setSelectedModel,
} = useStore(); } = useStore();
const t = translations[language].promptEnhancer;
const common = translations[language].common;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider]; const selectedModel = selectedModels[selectedProvider];
@@ -66,29 +71,37 @@ export default function PromptEnhancer() {
const handleEnhance = async () => { const handleEnhance = async () => {
if (!currentPrompt.trim()) { if (!currentPrompt.trim()) {
setError("Please enter a prompt to enhance"); setError(t.enterPromptError);
return; return;
} }
const apiKey = apiKeys[selectedProvider]; const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`${common.error}: ${common.configApiKey}`);
return; return;
} }
setProcessing(true); setProcessing(true);
setError(null); setError(null);
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try { try {
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel); const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[PromptEnhancer] Enhancement result:", result);
if (result.success && result.data) { if (result.success && result.data) {
setEnhancedPrompt(result.data); setEnhancedPrompt(result.data);
} else { } else {
setError(result.error || "Failed to enhance prompt"); console.error("[PromptEnhancer] Enhancement failed:", result.error);
setError(result.error || t.errorEnhance);
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "An error occurred"); console.error("[PromptEnhancer] Enhancement error:", err);
setError(err instanceof Error ? err.message : t.errorEnhance);
} finally { } finally {
setProcessing(false); setProcessing(false);
} }
@@ -109,21 +122,21 @@ export default function PromptEnhancer() {
}; };
return ( return (
<div className="mx-auto grid max-w-7xl gap-6 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"> <Card className="h-fit">
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Sparkles className="h-5 w-5" /> <Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
Input Prompt {t.title}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Enter your prompt and we'll enhance it for AI coding agents {t.description}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">AI Provider</label> <label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button <Button
key={provider} key={provider}
@@ -131,7 +144,7 @@ export default function PromptEnhancer() {
size="sm" size="sm"
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className={cn( className={cn(
"capitalize", "capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground" selectedProvider === provider && "bg-primary text-primary-foreground"
)} )}
> >
@@ -141,12 +154,12 @@ export default function PromptEnhancer() {
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">Model</label> <label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select <select
value={selectedModel} value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)} onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" 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) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
@@ -156,79 +169,79 @@ export default function PromptEnhancer() {
</select> </select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">Your Prompt</label> <label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
<Textarea <Textarea
placeholder="e.g., Create a user authentication system with JWT tokens" placeholder={t.placeholder}
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y" className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/> />
</div> </div>
{error && ( {error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error} {error}
{!apiKeys[selectedProvider] && ( {!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2"> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-xs">Configure API key in Settings</span> <span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div> </div>
)} )}
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1"> <Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
Enhancing... {common.generating}
</> </>
) : ( ) : (
<> <>
<Sparkles className="mr-2 h-4 w-4" /> <Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
Enhance Prompt {t.title}
</> </>
)} )}
</Button> </Button>
<Button variant="outline" onClick={handleClear} disabled={isProcessing}> <Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
Clear <span className="hidden sm:inline">{t.clear}</span>
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className={cn(!enhancedPrompt && "opacity-50")}> <Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
Enhanced Prompt {t.enhancedTitle}
</span> </span>
{enhancedPrompt && ( {enhancedPrompt && (
<Button variant="ghost" size="icon" onClick={handleCopy}> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? ( {copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
)} )}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Professional prompt ready for coding agents {t.enhancedDesc}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{enhancedPrompt ? ( {enhancedPrompt ? (
<div className="rounded-md border bg-muted/50 p-4"> <div className="rounded-md border bg-muted/50 p-3 lg:p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<pre className="whitespace-pre-wrap text-sm">{enhancedPrompt}</pre> <pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
</div> </div>
) : ( ) : (
<div className="flex h-[200px] items-center justify-center text-center text-sm text-muted-foreground"> <div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
Enhanced prompt will appear here {t.emptyState}
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -7,16 +7,19 @@ import { Input } from "@/components/ui/input";
import useStore from "@/lib/store"; import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance"; import modelAdapter from "@/lib/services/adapter-instance";
import { Save, Key, Server, Eye, EyeOff } from "lucide-react"; import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
import { translations } from "@/lib/i18n/translations";
export default function SettingsPanel() { export default function SettingsPanel() {
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore(); const { language, apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
const t = translations[language].settings;
const common = translations[language].common;
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({}); const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
const [isAuthLoading, setIsAuthLoading] = useState(false); const [isAuthLoading, setIsAuthLoading] = useState(false);
const handleSave = () => { const handleSave = () => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys)); localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
alert("API keys saved successfully!"); alert(t.keysSaved);
} }
}; };
@@ -77,10 +80,11 @@ export default function SettingsPanel() {
try { try {
const token = await modelAdapter.startQwenOAuth(); const token = await modelAdapter.startQwenOAuth();
setQwenTokens(token); setQwenTokens(token);
modelAdapter.updateQwenTokens(token);
} catch (error) { } catch (error) {
console.error("Qwen OAuth failed", error); console.error("Qwen OAuth failed", error);
window.alert( window.alert(
error instanceof Error ? error.message : "Qwen authentication failed" error instanceof Error ? error.message : t.qwenAuthFailed
); );
} finally { } finally {
setIsAuthLoading(false); setIsAuthLoading(false);
@@ -92,48 +96,48 @@ export default function SettingsPanel() {
}, []); }, []);
return ( return (
<div className="mx-auto max-w-3xl space-y-6"> <div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
<Card> <Card>
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Key className="h-5 w-5" /> <Key className="h-4 w-4 lg:h-5 lg:w-5" />
API Configuration {t.apiKeys}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Configure API keys for different AI providers {t.apiKeysDesc}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <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-sm font-medium"> <label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-4 w-4" /> <Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Qwen Code API Key Qwen Code API Key
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
type={showApiKey.qwen ? "text" : "password"} type={showApiKey.qwen ? "text" : "password"}
placeholder="Enter your Qwen API key" placeholder={t.enterKey("Qwen")}
value={apiKeys.qwen || ""} value={apiKeys.qwen || ""}
onChange={(e) => handleApiKeyChange("qwen", e.target.value)} onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
className="font-mono text-sm" className="font-mono text-xs lg:text-sm pr-10"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-0 top-0 h-full" className="absolute right-0 top-0 h-full w-9 lg:w-10"
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))} onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
> >
{showApiKey.qwen ? ( {showApiKey.qwen ? (
<EyeOff className="h-4 w-4" /> <EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<Eye className="h-4 w-4" /> <Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
<p className="text-xs text-muted-foreground flex-1"> <p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
Get API key from{" "} {t.getApiKey}{" "}
<a <a
href="https://help.aliyun.com/zh/dashscope/" href="https://help.aliyun.com/zh/dashscope/"
target="_blank" target="_blank"
@@ -146,53 +150,53 @@ export default function SettingsPanel() {
<Button <Button
variant={qwenTokens ? "secondary" : "outline"} variant={qwenTokens ? "secondary" : "outline"}
size="sm" size="sm"
className="h-8" className="h-7 lg:h-8 text-[10px] lg:text-xs w-full sm:w-auto"
onClick={handleQwenAuth} onClick={handleQwenAuth}
disabled={isAuthLoading} disabled={isAuthLoading}
> >
{isAuthLoading {isAuthLoading
? "Signing in..." ? t.signingIn
: qwenTokens : qwenTokens
? "Logout from Qwen" ? t.logoutQwen
: "Login with Qwen (OAuth)"} : t.loginQwen}
</Button> </Button>
</div> </div>
{qwenTokens && ( {qwenTokens && (
<p className="text-[10px] text-green-600 dark:text-green-400 font-medium"> <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> </p>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="flex items-center gap-2 text-sm font-medium"> <label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-4 w-4" /> <Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Ollama Cloud API Key Ollama Cloud API Key
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
type={showApiKey.ollama ? "text" : "password"} type={showApiKey.ollama ? "text" : "password"}
placeholder="Enter your Ollama API key" placeholder={t.enterKey("Ollama")}
value={apiKeys.ollama || ""} value={apiKeys.ollama || ""}
onChange={(e) => handleApiKeyChange("ollama", e.target.value)} onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
className="font-mono text-sm" className="font-mono text-xs lg:text-sm pr-10"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-0 top-0 h-full" className="absolute right-0 top-0 h-full w-9 lg:w-10"
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))} onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
> >
{showApiKey.ollama ? ( {showApiKey.ollama ? (
<EyeOff className="h-4 w-4" /> <EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<Eye className="h-4 w-4" /> <Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-[10px] lg:text-xs text-muted-foreground">
Get API key from{" "} {t.getApiKey}{" "}
<a <a
href="https://ollama.com/cloud" href="https://ollama.com/cloud"
target="_blank" target="_blank"
@@ -204,35 +208,35 @@ export default function SettingsPanel() {
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="flex items-center gap-2 text-sm font-medium"> <label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-4 w-4" /> <Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Z.AI Plan API Key Z.AI Plan API Key
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
type={showApiKey.zai ? "text" : "password"} type={showApiKey.zai ? "text" : "password"}
placeholder="Enter your Z.AI API key" placeholder={t.enterKey("Z.AI")}
value={apiKeys.zai || ""} value={apiKeys.zai || ""}
onChange={(e) => handleApiKeyChange("zai", e.target.value)} onChange={(e) => handleApiKeyChange("zai", e.target.value)}
className="font-mono text-sm" className="font-mono text-xs lg:text-sm pr-10"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-0 top-0 h-full" className="absolute right-0 top-0 h-full w-9 lg:w-10"
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))} onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
> >
{showApiKey.zai ? ( {showApiKey.zai ? (
<EyeOff className="h-4 w-4" /> <EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<Eye className="h-4 w-4" /> <Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-[10px] lg:text-xs text-muted-foreground">
Get API key from{" "} {t.getApiKey}{" "}
<a <a
href="https://docs.z.ai" href="https://docs.z.ai"
target="_blank" target="_blank"
@@ -244,45 +248,44 @@ export default function SettingsPanel() {
</p> </p>
</div> </div>
<Button onClick={handleSave} className="w-full"> <Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
<Save className="mr-2 h-4 w-4" /> <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> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle>Default Provider</CardTitle> <CardTitle className="text-base lg:text-lg">{t.defaultProvider}</CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Select your preferred AI provider {t.defaultProviderDesc}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="grid gap-3"> <div className="grid gap-2 lg:gap-3">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<button <button
key={provider} key={provider}
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 ${ 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
selectedProvider === provider ? "border-primary bg-primary/5"
? "border-primary bg-primary/5" : "border-border"
: "border-border" }`}
}`}
> >
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10"> <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-5 w-5 text-primary" /> <Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
</div> </div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<h3 className="font-medium capitalize">{provider}</h3> <h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
<p className="text-sm text-muted-foreground"> <p className="text-[10px] lg:text-sm text-muted-foreground truncate">
{provider === "qwen" && "Alibaba DashScope API"} {provider === "qwen" && t.qwenDesc}
{provider === "ollama" && "Ollama Cloud API"} {provider === "ollama" && t.ollamaDesc}
{provider === "zai" && "Z.AI Plan API"} {provider === "zai" && t.zaiDesc}
</p> </p>
</div> </div>
{selectedProvider === provider && ( {selectedProvider === provider && (
<div className="h-2 w-2 rounded-full bg-primary" /> <div className="h-2 w-2 rounded-full bg-primary flex-shrink-0" />
)} )}
</button> </button>
))} ))}
@@ -291,16 +294,16 @@ export default function SettingsPanel() {
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle>Data Privacy</CardTitle> <CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Your data handling preferences {t.dataPrivacyTitleDesc}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
<div className="rounded-md border bg-muted/30 p-4"> <div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
<p className="text-sm"> <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> </p>
</div> </div>
</CardContent> </CardContent>

View File

@@ -1,11 +1,13 @@
"use client"; "use client";
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import useStore from "@/lib/store"; import useStore from "@/lib/store";
import { Sparkles, FileText, ListTodo, Palette, History, Settings } 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 { 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 { interface SidebarProps {
currentView: View; currentView: View;
@@ -13,79 +15,149 @@ interface SidebarProps {
} }
export default function Sidebar({ currentView, onViewChange }: 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 = [ const menuItems = [
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles }, { id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
{ id: "prd" as View, label: "PRD Generator", icon: FileText }, { id: "prd" as View, label: t.prdGenerator, icon: FileText },
{ id: "action" as View, label: "Action Plan", icon: ListTodo }, { id: "action" as View, label: t.actionPlan, icon: ListTodo },
{ id: "uxdesigner" as View, label: "UX Designer Prompt", icon: Palette }, { id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
{ id: "history" as View, label: "History", icon: History, count: history.length }, { id: "slides" as View, label: t.slidesGen, icon: Presentation },
{ id: "settings" as View, label: "Settings", icon: Settings }, { 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 },
]; ];
return ( const handleViewChange = (view: View) => {
<aside className="flex h-screen w-64 flex-col border-r bg-card"> onViewChange(view);
<div className="border-b p-6"> setIsMobileMenuOpen(false);
<h1 className="flex items-center gap-2 text-xl font-bold"> };
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
PA const SidebarContent = () => (
</div> <>
PromptArch <div className="border-b p-4 lg:p-6">
</h1> <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" />
<span>{t.backToRommark}</span>
</a>
<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>
{t.title}
</h1>
</a>
<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>{t.viewOnGithub}</span>
</a>
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
{t.forkedFrom} <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
</p>
</div> </div>
<nav className="flex-1 space-y-1 p-4"> <nav className="flex-1 space-y-1 p-3 lg:p-4 overflow-y-auto">
{menuItems.map((item) => ( {menuItems.map((item) => (
<Button <Button
key={item.id} key={item.id}
variant={currentView === item.id ? "default" : "ghost"} variant={currentView === item.id ? "default" : "ghost"}
className={cn( className={cn(
"w-full justify-start gap-2", "w-full justify-start gap-2 h-9 lg:h-10 text-sm",
currentView === item.id && "bg-primary text-primary-foreground" currentView === item.id && "bg-primary text-primary-foreground"
)} )}
onClick={() => onViewChange(item.id)} onClick={() => handleViewChange(item.id)}
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
<span className="flex-1 text-left">{item.label}</span> <span className="flex-1 text-left truncate">{item.label}</span>
{item.count !== undefined && item.count > 0 && ( {item.count !== undefined && item.count > 0 && (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium"> <span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium text-primary">
{item.count} {item.count}
</span> </span>
)} )}
</Button> </Button>
))} ))}
<div className="mt-8 p-3 text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-4"> <div className="mt-4 p-2 lg:p-3 border-t border-border/50">
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p> <div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
<div className="space-y-1"> <Languages className="h-3 w-3" /> {t.language}
<p> </div>
GitHub: <a href="https://github.com/roman-ryzenadvanced/Custom-Engineered-Agents-and-Tools-for-Vibe-Coders" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a> <div className="flex flex-wrap gap-1">
</p> {(["en", "ru", "he"] as const).map((lang) => (
<p> <Button
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a> key={lang}
</p> variant={language === lang ? "default" : "outline"}
<p className="mt-2 text-[9px] opacity-80"> size="sm"
100% Developed using GLM 4.7 model on TRAE.AI IDE. className="h-7 px-2 text-[10px] uppercase font-bold"
</p> onClick={() => setLanguage(lang)}
<p className="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> {lang}
</p> </Button>
))}
</div> </div>
</div> </div>
</nav> </nav>
<div className="border-t p-4"> <div className="border-t p-3 lg:p-4 hidden lg:block">
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground"> <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-2 space-y-1"> <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> {t.tip1}</li>
<li> Copy enhanced prompts to your AI agent</li> <li> {t.tip2}</li>
<li> PRDs generate better action plans</li> <li> {t.tip3}</li>
<li> UX Designer Prompt for design tasks</li>
</ul> </ul>
</div> </div>
</div> </div>
</aside> </>
);
return (
<>
{/* 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.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">{t.title}</span>
</a>
<Button
variant="ghost"
size="icon"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="h-9 w-9"
>
{isMobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
</div>
{/* Mobile Menu Overlay */}
{isMobileMenuOpen && (
<div
className="lg:hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Mobile Slide-out Menu */}
<aside
className={cn(
"lg:hidden fixed top-0 left-0 z-50 flex h-full w-72 max-w-[80vw] flex-col border-r bg-card transition-transform duration-300 ease-in-out",
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<SidebarContent />
</aside>
{/* Desktop Sidebar */}
<aside className="hidden lg:flex h-screen w-64 flex-col border-r bg-card flex-shrink-0">
<SidebarContent />
</aside>
</>
); );
} }

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 modelAdapter from "@/lib/services/adapter-instance";
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react"; import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function UXDesignerPrompt() { export default function UXDesignerPrompt() {
const { const {
language,
currentPrompt, currentPrompt,
enhancedPrompt,
selectedProvider, selectedProvider,
selectedModels, selectedModels,
availableModels, availableModels,
@@ -27,6 +30,9 @@ export default function UXDesignerPrompt() {
setSelectedModel, setSelectedModel,
} = useStore(); } = useStore();
const t = translations[language].uxDesigner;
const common = translations[language].common;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null); const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
@@ -65,13 +71,15 @@ export default function UXDesignerPrompt() {
const handleGenerate = async () => { const handleGenerate = async () => {
if (!currentPrompt.trim()) { if (!currentPrompt.trim()) {
setError("Please enter an app description"); setError(t.enterDescriptionError);
return; return;
} }
const apiKey = apiKeys[selectedProvider]; const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`${common.error}: ${common.configApiKey}`);
return; return;
} }
@@ -79,17 +87,23 @@ export default function UXDesignerPrompt() {
setError(null); setError(null);
setGeneratedPrompt(null); setGeneratedPrompt(null);
console.log("[UXDesignerPrompt] Starting generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try { try {
const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel); const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[UXDesignerPrompt] Generation result:", result);
if (result.success && result.data) { if (result.success && result.data) {
setGeneratedPrompt(result.data); setGeneratedPrompt(result.data);
setEnhancedPrompt(result.data); setEnhancedPrompt(result.data);
} else { } else {
setError(result.error || "Failed to generate UX designer prompt"); console.error("[UXDesignerPrompt] Generation failed:", result.error);
setError(result.error || t.errorGenerate);
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "An error occurred"); console.error("[UXDesignerPrompt] Generation error:", err);
setError(err instanceof Error ? err.message : t.errorGenerate);
} finally { } finally {
setProcessing(false); setProcessing(false);
} }
@@ -111,21 +125,21 @@ export default function UXDesignerPrompt() {
}; };
return ( return (
<div className="mx-auto grid max-w-7xl gap-6 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"> <Card className="h-fit">
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Palette className="h-5 w-5" /> <Palette className="h-4 w-4 lg:h-5 lg:w-5" />
UX Designer Prompt {t.title}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Describe your app idea and get the BEST EVER prompt for UX design {t.description}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">AI Provider</label> <label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["ollama", "zai"] as const).map((provider) => ( {(["ollama", "zai"] as const).map((provider) => (
<Button <Button
key={provider} key={provider}
@@ -133,7 +147,7 @@ export default function UXDesignerPrompt() {
size="sm" size="sm"
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className={cn( className={cn(
"capitalize", "capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground" selectedProvider === provider && "bg-primary text-primary-foreground"
)} )}
> >
@@ -143,12 +157,12 @@ export default function UXDesignerPrompt() {
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">Model</label> <label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select <select
value={selectedModel} value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)} onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" 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) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
@@ -158,81 +172,83 @@ export default function UXDesignerPrompt() {
</select> </select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-sm font-medium">App Description</label> <label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
<Textarea <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} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y" className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/> />
<p className="text-xs text-muted-foreground"> <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> </p>
</div> </div>
{error && ( {error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error} {error}
{!apiKeys[selectedProvider] && ( {!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2"> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-xs">Configure API key in Settings</span> <span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div> </div>
)} )}
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1"> <Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <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-2 h-4 w-4" /> <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>
<Button variant="outline" onClick={handleClear} disabled={isProcessing}> <Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
Clear <span className="hidden sm:inline">{translations[language].promptEnhancer.clear}</span>
<span className="sm:hidden">×</span>
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className={cn(!generatedPrompt && "opacity-50")}> <Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
<CardHeader> <CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
Best Ever UX Prompt <span className="hidden sm:inline">{t.resultTitle}</span>
<span className="sm:hidden">{t.uxPromptMobile}</span>
</span> </span>
{generatedPrompt && ( {generatedPrompt && (
<Button variant="ghost" size="icon" onClick={handleCopy}> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? ( {copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
)} )}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Comprehensive UX design prompt ready for designers {t.resultDesc}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{generatedPrompt ? ( {generatedPrompt ? (
<div className="rounded-md border bg-muted/50 p-4"> <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-sm">{generatedPrompt}</pre> <pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{generatedPrompt}</pre>
</div> </div>
) : ( ) : (
<div className="flex h-[400px] items-center justify-center text-center text-sm text-muted-foreground"> <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">
Your comprehensive UX designer prompt will appear here {t.emptyState}
</div> </div>
)} )}
</CardContent> </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 };
}

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

1336
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

@@ -2,4 +2,6 @@ import ModelAdapter from "./model-adapter";
const adapter = new ModelAdapter(); const adapter = new ModelAdapter();
adapter["qwenService"]["initialize"]?.();
export default adapter; export default adapter;

View File

@@ -1,4 +1,4 @@
import type { ModelProvider, APIResponse, ChatMessage } from "@/types"; import type { ModelProvider, APIResponse, ChatMessage, AIAssistMessage } from "@/types";
import OllamaCloudService from "./ollama-cloud"; import OllamaCloudService from "./ollama-cloud";
import ZaiPlanService from "./zai-plan"; import ZaiPlanService from "./zai-plan";
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth"; import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
@@ -70,6 +70,23 @@ export class ModelAdapter {
return this.qwenService.getTokenInfo(); return this.qwenService.getTokenInfo();
} }
hasQwenAuth(): boolean {
return this.qwenService.hasOAuthToken();
}
private isProviderAuthenticated(provider: ModelProvider): boolean {
switch (provider) {
case "qwen":
return this.hasQwenAuth() || this.qwenService.hasApiKey();
case "ollama":
return this.ollamaService.hasAuth();
case "zai":
return this.zaiService.hasAuth();
default:
return false;
}
}
private buildFallbackProviders(...providers: ModelProvider[]): ModelProvider[] { private buildFallbackProviders(...providers: ModelProvider[]): ModelProvider[] {
const seen = new Set<ModelProvider>(); const seen = new Set<ModelProvider>();
return providers.filter((provider) => { return providers.filter((provider) => {
@@ -81,17 +98,46 @@ 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;
default:
return null;
}
}
private async callWithFallback<T>( private async callWithFallback<T>(
operation: (service: any) => Promise<APIResponse<T>>, operation: (service: any) => Promise<APIResponse<T>>,
providers: ModelProvider[] providers: ModelProvider[]
): Promise<APIResponse<T>> { ): Promise<APIResponse<T>> {
console.log("[ModelAdapter] Attempting providers in order:", providers);
let lastError: string | null = null;
for (const provider of providers) { for (const provider of providers) {
try { try {
console.log(`[ModelAdapter] Checking authentication for ${provider}...`);
if (!this.isProviderAuthenticated(provider)) {
console.log(`[ModelAdapter] Provider ${provider} is not authenticated, skipping`);
continue;
}
let service: any; let service: any;
console.log(`[ModelAdapter] Trying provider: ${provider}`);
switch (provider) { switch (provider) {
case "qwen": case "qwen":
service = this.qwenService; service = this.qwenService;
console.log("[ModelAdapter] Qwen service:", {
hasApiKey: !!this.qwenService["apiKey"],
hasToken: !!this.qwenService.getTokenInfo()?.accessToken
});
break; break;
case "ollama": case "ollama":
service = this.ollamaService; service = this.ollamaService;
@@ -102,17 +148,30 @@ export class ModelAdapter {
} }
const result = await operation(service); const result = await operation(service);
console.log(`[ModelAdapter] Provider ${provider} result:`, result);
if (result.success) { if (result.success) {
console.log(`[ModelAdapter] Success with provider: ${provider}`);
return result; return result;
} }
if (result.error) {
lastError = result.error;
}
} catch (error) { } catch (error) {
console.error(`Error with ${provider}:`, error); const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[ModelAdapter] Error with ${provider}:`, errorMessage);
lastError = errorMessage || lastError;
} }
} }
const finalError = lastError
? `All providers failed: ${lastError}`
: "All providers failed. Please configure API key in Settings";
console.error(`[ModelAdapter] ${finalError}`);
return { return {
success: false, success: false,
error: "All providers failed", error: finalError,
}; };
} }
@@ -140,6 +199,134 @@ export class ModelAdapter {
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers); 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");
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");
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");
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");
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");
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");
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( async chatCompletion(
messages: ChatMessage[], messages: ChatMessage[],
model: string, model: string,

View File

@@ -1,12 +1,13 @@
import type { ChatMessage, APIResponse } from "@/types"; import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
export interface OllamaCloudConfig { export interface OllamaCloudConfig {
apiKey?: string; apiKey?: string;
endpoint?: string; endpoint?: string;
} }
const LOCAL_MODELS_URL = "/api/ollama/models"; const BASE_PATH = "/tools/promptarch";
const LOCAL_CHAT_URL = "/api/ollama/chat"; const LOCAL_MODELS_URL = `${BASE_PATH}/api/ollama/models`;
const LOCAL_CHAT_URL = `${BASE_PATH}/api/ollama/chat`;
const DEFAULT_MODELS = [ const DEFAULT_MODELS = [
"gpt-oss:120b", "gpt-oss:120b",
"llama3.1:latest", "llama3.1:latest",
@@ -53,6 +54,10 @@ export class OllamaCloudService {
}; };
} }
hasAuth(): boolean {
return !!this.config.apiKey;
}
private ensureApiKey(): string { private ensureApiKey(): string {
if (this.config.apiKey) { if (this.config.apiKey) {
return this.config.apiKey; return this.config.apiKey;
@@ -158,6 +163,735 @@ export class OllamaCloudService {
getAvailableModels(): string[] { getAvailableModels(): string[] {
return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS; return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS;
} }
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
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.
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
Return ONLY the enhanced prompt, no explanations or extra text.`,
};
const userMessage: ChatMessage = {
role: "user",
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
};
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
}
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)
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 || "gpt-oss:120b");
}
async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> {
const systemMessage: ChatMessage = {
role: "system",
content: `You are an expert technical lead and project manager. Generate a detailed action plan based on PRD.
Structure of action plan with:
1. Task breakdown with priorities (High/Medium/Low)
2. Dependencies between tasks
3. Estimated effort for each task
4. Recommended frameworks and technologies
5. Architecture guidelines and best practices
Include specific recommendations for:
- Frontend frameworks
- Backend architecture
- Database choices
- Authentication/authorization
- Deployment strategy`,
};
const userMessage: ChatMessage = {
role: "user",
content: `Generate an action plan based on this PRD:\n\n${prd}`,
};
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
}
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
const systemMessage: ChatMessage = {
role: "system",
content: `You are a world-class UX/UI designer with deep expertise in human-centered design principles, user research, interaction design, visual design systems, and modern design tools (Figma, Sketch, Adobe XD).
Your task is to create an exceptional, detailed prompt for generating best possible UX design for a given app description.
Generate a comprehensive UX design prompt that includes:
1. USER RESEARCH & PERSONAS
- Primary target users and their motivations
- User pain points and needs
- User journey maps
- Persona archetypes with demographics and goals
2. INFORMATION ARCHITECTURE
- Content hierarchy and organization
- Navigation structure and patterns
- User flows and key pathways
- Site map or app structure
3. VISUAL DESIGN SYSTEM
- Color palette recommendations (primary, secondary, accent, neutral)
- Typography hierarchy and font pairings
- Component library approach
- Spacing, sizing, and layout grids
- Iconography style and set
4. INTERACTION DESIGN
- Micro-interactions and animations
- Gesture patterns for touch interfaces
- Loading states and empty states
- Error handling and feedback mechanisms
- Accessibility considerations (WCAG compliance)
5. KEY SCREENS & COMPONENTS
- Core screens that need detailed design
- Critical components (buttons, forms, cards, navigation)
- Data visualization needs
- Responsive design requirements (mobile, tablet, desktop)
6. DESIGN DELIVERABLES
- Wireframes vs. high-fidelity mockups
- Design system documentation needs
- Prototyping requirements
- Handoff specifications for developers
7. COMPETITIVE INSIGHTS
- Design patterns from successful apps in this category
- Opportunities to differentiate
- Modern design trends to consider
The output should be a detailed, actionable prompt that a designer or AI image generator can use to create world-class UX designs.
Make's prompt specific, inspiring, and comprehensive. Use professional UX terminology.`,
};
const userMessage: ChatMessage = {
role: "user",
content: `Create a BEST EVER UX design prompt for this app:\n\n${appDescription}`,
};
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.
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: 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; export default OllamaCloudService;

View File

@@ -1,16 +1,22 @@
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 DEFAULT_QWEN_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
const TOKEN_STORAGE_KEY = "promptarch-qwen-tokens"; const TOKEN_STORAGE_KEY = "promptarch-qwen-tokens";
function getOAuthBaseUrl(): string { function getOAuthBaseUrl(): string {
const basePath = '/tools/promptarch';
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
return `${window.location.origin}/api/qwen`; const origin = window.location.origin;
return `${origin}${basePath}/api/qwen`;
} }
if (process.env.NEXT_PUBLIC_SITE_URL) { if (process.env.NEXT_PUBLIC_SITE_URL) {
return `${process.env.NEXT_PUBLIC_SITE_URL}/api/qwen`; const siteUrl = process.env.NEXT_PUBLIC_SITE_URL.replace(/\/$/, "");
if (siteUrl.endsWith(basePath)) {
return `${siteUrl}/api/qwen`;
}
return `${siteUrl}${basePath}/api/qwen`;
} }
return "/api/qwen"; return `${basePath}/api/qwen`;
} }
export interface QwenOAuthConfig { export interface QwenOAuthConfig {
@@ -68,10 +74,20 @@ export class QwenOAuthService {
this.apiKey = apiKey; this.apiKey = apiKey;
} }
hasApiKey(): boolean {
return !!this.apiKey;
}
hasOAuthToken(): boolean {
return !!this.getTokenInfo()?.accessToken;
}
/** /**
* Build default headers for Qwen completions (includes OAuth token refresh). * Build default headers for Qwen completions (includes OAuth token refresh).
*/ */
private async getRequestHeaders(): Promise<Record<string, string>> { private async getRequestHeaders(): Promise<Record<string, string>> {
console.log("[QwenOAuth] Getting request headers...");
const token = await this.getValidToken(); const token = await this.getValidToken();
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -79,14 +95,17 @@ export class QwenOAuthService {
if (token?.accessToken) { if (token?.accessToken) {
headers["Authorization"] = `Bearer ${token.accessToken}`; headers["Authorization"] = `Bearer ${token.accessToken}`;
console.log("[QwenOAuth] Using OAuth token for authorization");
return headers; return headers;
} }
if (this.apiKey) { if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`; headers["Authorization"] = `Bearer ${this.apiKey}`;
console.log("[QwenOAuth] Using API key for authorization");
return headers; return headers;
} }
console.error("[QwenOAuth] No OAuth token or API key available");
throw new Error("Please configure a Qwen API key or authenticate via OAuth."); throw new Error("Please configure a Qwen API key or authenticate via OAuth.");
} }
@@ -96,8 +115,11 @@ export class QwenOAuthService {
private getEffectiveEndpoint(): string { private getEffectiveEndpoint(): string {
const resourceUrl = this.token?.resourceUrl; const resourceUrl = this.token?.resourceUrl;
if (resourceUrl) { if (resourceUrl) {
return this.normalizeResourceUrl(resourceUrl); const normalized = this.normalizeResourceUrl(resourceUrl);
console.log("[Qwen] Using resource URL:", normalized);
return normalized;
} }
console.log("[Qwen] Using default endpoint:", this.endpoint);
return this.endpoint; return this.endpoint;
} }
@@ -109,7 +131,12 @@ export class QwenOAuthService {
const withProtocol = trimmed.startsWith("http") ? trimmed : `https://${trimmed}`; const withProtocol = trimmed.startsWith("http") ? trimmed : `https://${trimmed}`;
const cleaned = withProtocol.replace(/\/$/, ""); const cleaned = withProtocol.replace(/\/$/, "");
return cleaned.endsWith("/v1") ? cleaned : `${cleaned}/v1`;
if (cleaned.endsWith("/v1") || cleaned.endsWith("/compatible-mode/v1")) {
return cleaned;
}
return `${cleaned}/v1`;
} }
private hydrateTokens() { private hydrateTokens() {
@@ -132,6 +159,7 @@ export class QwenOAuthService {
private getStoredToken(): QwenOAuthToken | null { private getStoredToken(): QwenOAuthToken | null {
this.hydrateTokens(); this.hydrateTokens();
console.log("[QwenOAuth] Retrieved stored token:", this.token ? { hasAccessToken: !!this.token.accessToken, expiresAt: this.token.expiresAt } : null);
return this.token; return this.token;
} }
@@ -229,8 +257,18 @@ export class QwenOAuthService {
this.storageHydrated = true; this.storageHydrated = true;
} }
/**
* Initialize the service and hydrate tokens from storage.
*/
initialize(): void {
console.log("[QwenOAuth] Initializing service...");
this.hydrateTokens();
}
getTokenInfo(): QwenOAuthToken | null { getTokenInfo(): QwenOAuthToken | null {
return this.getStoredToken(); this.hydrateTokens();
console.log("[QwenOAuth] getTokenInfo called, returning:", this.token ? { hasAccessToken: !!this.token.accessToken, expiresAt: this.token.expiresAt } : null);
return this.token;
} }
/** /**
@@ -353,12 +391,30 @@ export class QwenOAuthService {
} }
private parseTokenResponse(data: any): QwenOAuthToken { private parseTokenResponse(data: any): QwenOAuthToken {
return { console.log("[QwenOAuth] Token response received:", data);
const token: QwenOAuthToken = {
accessToken: data.access_token, accessToken: data.access_token,
refreshToken: data.refresh_token, refreshToken: data.refresh_token,
resourceUrl: data.resource_url,
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined, expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined,
}; };
if (data.resource_url) {
token.resourceUrl = data.resource_url;
console.log("[QwenOAuth] Using resource_url from response:", data.resource_url);
} else if (data.endpoint) {
token.resourceUrl = data.endpoint;
console.log("[QwenOAuth] Using endpoint from response:", data.endpoint);
} else if (data.resource_server) {
token.resourceUrl = `https://${data.resource_server}/compatible-mode/v1`;
console.log("[QwenOAuth] Using resource_server from response:", data.resource_server);
} else {
console.log("[QwenOAuth] No resource_url/endpoint in response, will use default Qwen endpoint");
console.log("[QwenOAuth] Available fields in response:", Object.keys(data));
}
console.log("[QwenOAuth] Parsed token:", { hasAccessToken: !!token.accessToken, hasRefreshToken: !!token.refreshToken, hasResourceUrl: !!token.resourceUrl, expiresAt: token.expiresAt });
return token;
} }
/** /**
@@ -390,17 +446,24 @@ export class QwenOAuthService {
async chatCompletion( async chatCompletion(
messages: ChatMessage[], messages: ChatMessage[],
model: string = "qwen-coder-plus", model: string = "coder-model",
stream: boolean = false stream: boolean = false
): Promise<APIResponse<string>> { ): Promise<APIResponse<string>> {
try { try {
const headers = await this.getRequestHeaders(); const headers = await this.getRequestHeaders();
const url = `${this.getEffectiveEndpoint()}/chat/completions`; const baseUrl = this.getEffectiveEndpoint();
const url = `${this.oauthBaseUrl}/chat`;
console.log("[Qwen] Chat completion request:", { url, model, hasAuth: !!headers.Authorization });
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
headers, headers: {
"Content-Type": "application/json",
Authorization: headers.Authorization || "",
},
body: JSON.stringify({ body: JSON.stringify({
endpoint: baseUrl,
model, model,
messages, messages,
stream, stream,
@@ -409,6 +472,7 @@ export class QwenOAuthService {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error("[Qwen] Chat completion failed:", response.status, response.statusText, errorText);
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`); throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
} }
@@ -447,7 +511,7 @@ Return ONLY the enhanced prompt, no explanations or extra text.`,
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`, content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
}; };
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus"); return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
} }
async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> { async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> {
@@ -471,7 +535,7 @@ Use clear, specific language suitable for development teams.`,
content: `Generate a PRD for this idea:\n\n${idea}`, content: `Generate a PRD for this idea:\n\n${idea}`,
}; };
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus"); return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
} }
async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> { async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> {
@@ -499,7 +563,7 @@ Include specific recommendations for:
content: `Generate an action plan based on this PRD:\n\n${prd}`, content: `Generate an action plan based on this PRD:\n\n${prd}`,
}; };
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus"); return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
} }
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> { async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
@@ -564,33 +628,584 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
content: `Create a BEST EVER UX design prompt for this app:\n\n${appDescription}`, content: `Create a BEST EVER UX design prompt for this app:\n\n${appDescription}`,
}; };
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus"); return this.chatCompletion([systemMessage, userMessage], model || "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 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.
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: 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[]>> { async listModels(): Promise<APIResponse<string[]>> {
const models = [ return { success: true, data: this.getAvailableModels() };
"qwen-coder-plus",
"qwen-coder-turbo",
"qwen-coder-lite",
"qwen-plus",
"qwen-turbo",
"qwen-max",
];
return { success: true, data: models };
} }
getAvailableModels(): string[] { getAvailableModels(): string[] {
return [ return [
"qwen-coder-plus", "coder-model",
"qwen-coder-turbo",
"qwen-coder-lite",
"qwen-plus",
"qwen-turbo",
"qwen-max",
]; ];
} }
} }
const qwenOAuthService = new QwenOAuthService(); const qwenOAuthService = new QwenOAuthService();
export default qwenOAuthService; export default qwenOAuthService;
export { qwenOAuthService };

View File

@@ -1,4 +1,4 @@
import type { ChatMessage, APIResponse } from "@/types"; import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
export interface ZaiPlanConfig { export interface ZaiPlanConfig {
apiKey?: string; apiKey?: string;
@@ -17,6 +17,10 @@ export class ZaiPlanService {
}; };
} }
hasAuth(): boolean {
return !!this.config.apiKey;
}
private getHeaders(): Record<string, string> { private getHeaders(): Record<string, string> {
return { return {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -247,6 +251,647 @@ Make the prompt specific, inspiring, and comprehensive. Use professional UX term
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true); 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.
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: 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; export default ZaiPlanService;

View File

@@ -1,11 +1,30 @@
import { create } from "zustand"; 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 AppState { interface AppState {
currentPrompt: string; currentPrompt: string;
enhancedPrompt: string | null; enhancedPrompt: string | null;
prd: PRD | null; prd: PRD | null;
actionPlan: ActionPlan | 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; selectedProvider: ModelProvider;
selectedModels: Record<ModelProvider, string>; selectedModels: Record<ModelProvider, string>;
availableModels: Record<ModelProvider, string[]>; availableModels: Record<ModelProvider, string[]>;
@@ -15,6 +34,7 @@ interface AppState {
refreshToken?: string; refreshToken?: string;
expiresAt?: number; expiresAt?: number;
} | null; } | null;
githubToken?: string | null;
isProcessing: boolean; isProcessing: boolean;
error: string | null; error: string | null;
history: { history: {
@@ -27,11 +47,26 @@ interface AppState {
setEnhancedPrompt: (enhanced: string | null) => void; setEnhancedPrompt: (enhanced: string | null) => void;
setPRD: (prd: PRD) => void; setPRD: (prd: PRD) => void;
setActionPlan: (plan: ActionPlan) => 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; setSelectedProvider: (provider: ModelProvider) => void;
setSelectedModel: (provider: ModelProvider, model: string) => void; setSelectedModel: (provider: ModelProvider, model: string) => void;
setAvailableModels: (provider: ModelProvider, models: string[]) => void; setAvailableModels: (provider: ModelProvider, models: string[]) => void;
setApiKey: (provider: ModelProvider, key: string) => void; setApiKey: (provider: ModelProvider, key: string) => void;
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void; setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
setGithubToken: (token: string | null) => void;
setProcessing: (processing: boolean) => void; setProcessing: (processing: boolean) => void;
setError: (error: string | null) => void; setError: (error: string | null) => void;
addToHistory: (prompt: string) => void; addToHistory: (prompt: string) => void;
@@ -44,14 +79,28 @@ const useStore = create<AppState>((set) => ({
enhancedPrompt: null, enhancedPrompt: null,
prd: null, prd: null,
actionPlan: 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", selectedProvider: "qwen",
selectedModels: { selectedModels: {
qwen: "qwen-coder-plus", qwen: "coder-model",
ollama: "gpt-oss:120b", ollama: "gpt-oss:120b",
zai: "glm-4.7", zai: "glm-4.7",
}, },
availableModels: { availableModels: {
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"], qwen: ["coder-model"],
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"], 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"], zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
}, },
@@ -60,6 +109,7 @@ const useStore = create<AppState>((set) => ({
ollama: "", ollama: "",
zai: "", zai: "",
}, },
githubToken: null,
isProcessing: false, isProcessing: false,
error: null, error: null,
history: [], history: [],
@@ -68,6 +118,51 @@ const useStore = create<AppState>((set) => ({
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }), setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
setPRD: (prd) => set({ prd }), setPRD: (prd) => set({ prd }),
setActionPlan: (plan) => set({ actionPlan: plan }), 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 }), setSelectedProvider: (provider) => set({ selectedProvider: provider }),
setSelectedModel: (provider, model) => setSelectedModel: (provider, model) =>
set((state) => ({ set((state) => ({
@@ -82,6 +177,7 @@ const useStore = create<AppState>((set) => ({
apiKeys: { ...state.apiKeys, [provider]: key }, apiKeys: { ...state.apiKeys, [provider]: key },
})), })),
setQwenTokens: (tokens) => set({ qwenTokens: tokens }), setQwenTokens: (tokens) => set({ qwenTokens: tokens }),
setGithubToken: (token) => set({ githubToken: token }),
setProcessing: (processing) => set({ isProcessing: processing }), setProcessing: (processing) => set({ isProcessing: processing }),
setError: (error) => set({ error }), setError: (error) => set({ error }),
addToHistory: (prompt) => addToHistory: (prompt) =>
@@ -102,6 +198,17 @@ const useStore = create<AppState>((set) => ({
enhancedPrompt: null, enhancedPrompt: null,
prd: null, prd: null,
actionPlan: null, actionPlan: null,
slidesPresentation: null,
googleAdsResult: null,
magicWandResult: null,
marketResearchResult: null,
aiAssistTabs: [{
id: "default",
title: "New Chat",
history: [],
currentAgent: "general"
}],
activeTabId: "default",
error: null, error: null,
}), }),
})); }));

View File

@@ -1,6 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
basePath: '/tools/promptarch',
trailingSlash: true,
}; };
module.exports = nextConfig; module.exports = nextConfig;

896
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,15 +9,19 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-tabs": "^1.1.13",
"@types/node": "^22.10.1", "@types/node": "^22.10.1",
"@types/react": "^19.0.1", "@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.16.0", "eslint": "^9.16.0",
"eslint-config-next": "^15.0.3", "eslint-config-next": "^15.0.3",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^15.0.3", "next": "^16.1.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@@ -29,15 +33,33 @@
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"xlsx": "^0.18.5",
"zod": "^4.2.1", "zod": "^4.2.1",
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/node": "^22.10.1", "@types/node": "^22.10.1",
"@types/react": "^19.0.1", "@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2" "@types/react-dom": "^19.0.2"
}, },
"keywords": [], "keywords": [
"author": "", "ai",
"license": "ISC" "prompt-engineering",
"prd-generator",
"nextjs",
"qwen",
"ollama",
"zai"
],
"author": "Roman | RyzenAdvanced <https://github.com/roman-ryzenadvanced>",
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.rommark.dev/admin/PromptArch.git"
},
"bugs": {
"url": "https://github.rommark.dev/admin/PromptArch/issues"
},
"homepage": "https://github.rommark.dev/admin/PromptArch"
} }

View File

@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -19,9 +23,19 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
"./*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }

View File

@@ -91,3 +91,245 @@ export interface ChatMessage {
role: "system" | "user" | "assistant"; role: "system" | "user" | "assistant";
content: string; 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";

View File

@@ -4,9 +4,6 @@
"framework": "nextjs", "framework": "nextjs",
"devCommand": "npm run dev", "devCommand": "npm run dev",
"env": { "env": {
"NEXT_PUBLIC_SITE_URL": { "NEXT_PUBLIC_SITE_URL": "https://traetlzlxn2t.vercel.app"
"description": "The production URL of your app (e.g., https://your-app.vercel.app)",
"value": "https://your-app.vercel.app"
}
} }
} }