Compare commits

...

45 Commits

46 changed files with 9248 additions and 917 deletions

View File

@@ -14,3 +14,7 @@ OLLAMA_ENDPOINT=https://ollama.com/api
ZAI_API_KEY=
ZAI_GENERAL_ENDPOINT=https://api.z.ai/api/paas/v4
ZAI_CODING_ENDPOINT=https://api.z.ai/api/coding/paas/v4
# Site Configuration (Required for OAuth in production)
# Set to your production URL (e.g., https://your-app.vercel.app)
NEXT_PUBLIC_SITE_URL=http://localhost:6002

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 🚀
> **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).**
---
> **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.
**Developed by [Roman | RyzenAdvanced](https://github.com/roman-ryzenadvanced)**
- 📦 **GitHub Repository**: [roman-ryzenadvanced/PromptArch-the-prompt-enhancer](https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer)
- 📮 **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
## 🌟 Visual Overview
### 🛠 Core Capabilities
@@ -56,21 +61,20 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an e
- **Components**: [shadcn/ui](https://ui.shadcn.com/)
- **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**
- GitHub: [roman-ryzenadvanced](https://github.com/roman-ryzenadvanced)
- Telegram: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
**Forked from**: [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix)
- This project is a visual and architectural evolution of the Clavix framework
- Clavix focuses on agentic-first Markdown templates
- PromptArch provides a centralized web interface with advanced model orchestration
---
*100% Developed using GLM 4.7 model on TRAE.AI IDE.*
- **Styling**: TailwindCSS
- **UI Components**: shadcn/ui + Radix UI
- **State Management**: Zustand
- **Forms**: React Hook Form + Zod
- **Icons**: Lucide React
**Development Platform**: [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW)
- 100% AI-assisted development using TRAE.AI's advanced coding capabilities
- Learn more about the architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW)
## Development

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

@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from "next/server";
import { normalizeOllamaBase, DEFAULT_OLLAMA_BASE } from "../constants";
const API_PREFIX = "/api";
function getApiKey(request: NextRequest): string | null {
return request.headers.get("x-ollama-api-key");
}
function getBaseUrl(request: NextRequest): string {
const header = request.headers.get("x-ollama-endpoint");
if (header && header.trim().length > 0) {
return normalizeOllamaBase(header);
}
return DEFAULT_OLLAMA_BASE;
}
export async function POST(request: NextRequest) {
const apiKey = getApiKey(request);
if (!apiKey) {
return NextResponse.json(
{ error: "Ollama API key is required" },
{ status: 401 }
);
}
const body = await request.json();
const baseUrl = getBaseUrl(request);
const targetUrl = `${baseUrl}${API_PREFIX}/chat`;
try {
const response = await fetch(targetUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const payload = await response.text();
return NextResponse.json(
{ error: "Ollama chat request failed", details: payload },
{ status: response.status }
);
}
// If stream is requested, pipe the response body
if (body.stream) {
return new Response(response.body, {
headers: {
"Content-Type": "application/x-ndjson",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
const payload = await response.json();
return NextResponse.json(payload);
} catch (error) {
console.error("Ollama chat proxy failed", error);
return NextResponse.json(
{ error: "Ollama chat request failed" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,7 @@
export const DEFAULT_OLLAMA_BASE = process.env.NEXT_PUBLIC_OLLAMA_ENDPOINT || process.env.OLLAMA_ENDPOINT || "https://ollama.com";
export function normalizeOllamaBase(url?: string): string {
if (!url) return DEFAULT_OLLAMA_BASE.replace(/\/$/, "");
const trimmed = url.trim();
if (!trimmed) return DEFAULT_OLLAMA_BASE.replace(/\/$/, "");
return trimmed.replace(/\/$/, "");
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from "next/server";
import { normalizeOllamaBase, DEFAULT_OLLAMA_BASE } from "../constants";
const API_PREFIX = "/api";
function getApiKey(request: NextRequest): string | null {
return request.headers.get("x-ollama-api-key");
}
function getBaseUrl(request: NextRequest): string {
const header = request.headers.get("x-ollama-endpoint");
if (header && header.trim().length > 0) {
return normalizeOllamaBase(header);
}
return DEFAULT_OLLAMA_BASE;
}
async function fetchModelNames(url: string, apiKey: string): Promise<string[]> {
const response = await fetch(`${url}`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => "Failed to parse");
throw new Error(`${response.status} ${response.statusText} - ${errorText}`);
}
const json = await response.json().catch(() => null);
const candidates = Array.isArray(json?.models)
? json.models
: Array.isArray(json?.data)
? json.data
: Array.isArray(json)
? json
: [];
const names: string[] = [];
for (const entry of candidates) {
if (!entry) continue;
const name = entry.name || entry.model || entry.id;
if (typeof name === "string" && name.length > 0) {
names.push(name);
}
}
return names;
}
export async function GET(request: NextRequest) {
const apiKey = getApiKey(request);
if (!apiKey) {
return NextResponse.json(
{ error: "Ollama API key is required" },
{ status: 401 }
);
}
const baseUrl = getBaseUrl(request);
const primaryUrl = `${baseUrl}${API_PREFIX}/v1/models`;
const fallbackUrl = `${baseUrl}${API_PREFIX}/tags`;
try {
const primaryModels = await fetchModelNames(primaryUrl, apiKey);
if (primaryModels.length > 0) {
return NextResponse.json({ models: primaryModels });
}
} catch (error) {
console.warn("[Ollama] Primary model fetch failed:", error);
}
try {
const fallbackModels = await fetchModelNames(fallbackUrl, apiKey);
if (fallbackModels.length > 0) {
return NextResponse.json({ models: fallbackModels });
}
} catch (error) {
console.warn("[Ollama] Fallback model fetch failed:", error);
}
return NextResponse.json(
{ models: [] },
{ status: 502 }
);
}

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

22
app/api/qwen/constants.ts Normal file
View File

@@ -0,0 +1,22 @@
export const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai";
export const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
export const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
export const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
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_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

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from "next/server";
import {
QWEN_OAUTH_CLIENT_ID,
QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
QWEN_OAUTH_SCOPE,
createQwenHeaders,
} from "../../constants";
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const { code_challenge, code_challenge_method } = body || {};
if (!code_challenge || !code_challenge_method) {
return NextResponse.json(
{ error: "code_challenge and code_challenge_method are required" },
{ status: 400 }
);
}
console.log(`[Qwen Device Auth] Calling ${QWEN_OAUTH_DEVICE_CODE_ENDPOINT}...`);
const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
method: "POST",
headers: createQwenHeaders("application/x-www-form-urlencoded"),
body: new URLSearchParams({
client_id: QWEN_OAUTH_CLIENT_ID,
scope: QWEN_OAUTH_SCOPE,
code_challenge,
code_challenge_method,
}),
});
const payload = await response.text();
console.log(`[Qwen Device Auth] Response status: ${response.status}`);
let data;
try {
data = JSON.parse(payload);
} catch {
console.error(`[Qwen Device Auth] Failed to parse response: ${payload}`);
data = { error: payload || "Unknown error from Qwen" };
}
if (!response.ok) {
console.warn(`[Qwen Device Auth] Error response:`, data);
}
return NextResponse.json(data, { status: response.status });
} catch (error) {
console.error("Qwen device authorization failed", error);
return NextResponse.json(
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Device authorization failed" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from "next/server";
import {
QWEN_OAUTH_CLIENT_ID,
QWEN_OAUTH_TOKEN_ENDPOINT,
createQwenHeaders,
} from "../../constants";
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const { refresh_token } = body || {};
if (!refresh_token) {
return NextResponse.json(
{ error: "refresh_token is required" },
{ status: 400 }
);
}
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
method: "POST",
headers: createQwenHeaders("application/x-www-form-urlencoded"),
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token,
client_id: QWEN_OAUTH_CLIENT_ID,
}),
});
const payload = await response.text();
let data;
try {
data = JSON.parse(payload);
} catch {
data = { error: payload || "Unknown error from Qwen" };
}
return NextResponse.json(data, { status: response.status });
} catch (error) {
console.error("Qwen token refresh failed", error);
return NextResponse.json(
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Token refresh failed" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from "next/server";
import {
QWEN_OAUTH_CLIENT_ID,
QWEN_OAUTH_DEVICE_GRANT_TYPE,
QWEN_OAUTH_TOKEN_ENDPOINT,
createQwenHeaders,
} from "../../constants";
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const { device_code, code_verifier } = body || {};
if (!device_code || !code_verifier) {
return NextResponse.json(
{ error: "device_code and code_verifier are required" },
{ status: 400 }
);
}
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, {
method: "POST",
headers: createQwenHeaders("application/x-www-form-urlencoded"),
body: new URLSearchParams({
grant_type: QWEN_OAUTH_DEVICE_GRANT_TYPE,
client_id: QWEN_OAUTH_CLIENT_ID,
device_code,
code_verifier,
}),
});
const payload = await response.text();
console.log(`[Qwen Token Poll] Response status: ${response.status}`);
let data;
try {
data = JSON.parse(payload);
} catch {
console.error(`[Qwen Token Poll] Failed to parse response: ${payload}`);
data = { error: payload || "Unknown error from Qwen" };
}
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) {
console.error("Qwen token poll failed", error);
return NextResponse.json(
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Token poll failed" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
import { QWEN_OAUTH_BASE_URL, createQwenHeaders } from "../constants";
export async function GET(request: NextRequest) {
try {
const authorization = request.headers.get("authorization");
if (!authorization || !authorization.startsWith("Bearer ")) {
return NextResponse.json(
{ error: "Authorization required" },
{ status: 401 }
);
}
const token = authorization.slice(7);
const userResponse = await fetch(`${QWEN_OAUTH_BASE_URL}/api/v1/user`, {
headers: {
...createQwenHeaders(),
Authorization: `Bearer ${token}`,
},
});
if (!userResponse.ok) {
const errorText = await userResponse.text();
let errorData;
try {
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();
return NextResponse.json({ user: userData });
} catch (error) {
console.error("Qwen user info failed", error);
return NextResponse.json(
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Failed to fetch user info" },
{ 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;
}
body {
@apply bg-background text-foreground;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Mobile optimizations */
html {
scroll-behavior: smooth;
-webkit-tap-highlight-color: transparent;
}
/* Better touch targets */
button,
a,
[role="button"] {
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 { Inter } from "next/font/google";
import { Roboto } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
const roboto = Roboto({
subsets: ["latin"],
weight: ["300", "400", "500", "700"]
});
export const metadata: Metadata = {
title: "PromptArch - AI Prompt Engineering Platform",
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({
children,
}: Readonly<{
@@ -16,7 +22,11 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={roboto.className}>
<LocaleProvider>
{children}
</LocaleProvider>
</body>
</html>
);
}

View File

@@ -6,60 +6,21 @@ import type { View } from "@/components/Sidebar";
import PromptEnhancer from "@/components/PromptEnhancer";
import PRDGenerator from "@/components/PRDGenerator";
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
import SlidesGenerator from "@/components/SlidesGenerator";
import GoogleAdsGenerator from "@/components/GoogleAdsGenerator";
import MarketResearcher from "@/components/MarketResearcher";
import AIAssist from "@/components/AIAssist";
import HistoryPanel from "@/components/HistoryPanel";
import SettingsPanel from "@/components/SettingsPanel";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
export default function Home() {
const [currentView, setCurrentView] = useState<View>("enhance");
const { setQwenTokens, setApiKey } = useStore();
useEffect(() => {
// Handle OAuth callback
if (typeof window !== "undefined") {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (code) {
// In a real app, you would exchange the code for tokens here
// Since we don't have a backend or real client secret, we'll simulate it
console.log("OAuth code received:", code);
// Mock token exchange
const mockAccessToken = "mock_access_token_" + Math.random().toString(36).substr(2, 9);
const tokens = {
accessToken: mockAccessToken,
expiresAt: Date.now() + 3600 * 1000, // 1 hour
};
setQwenTokens(tokens);
modelAdapter.setQwenOAuthTokens(tokens.accessToken, undefined, 3600);
// Save to localStorage
localStorage.setItem("promptarch-qwen-tokens", JSON.stringify(tokens));
// Clear the code from URL
window.history.replaceState({}, document.title, window.location.pathname);
// Switch to settings to show success (optional)
setCurrentView("settings");
}
// Load tokens from localStorage on init
const savedTokens = localStorage.getItem("promptarch-qwen-tokens");
if (savedTokens) {
try {
const tokens = JSON.parse(savedTokens);
if (tokens.expiresAt > Date.now()) {
setQwenTokens(tokens);
modelAdapter.setQwenOAuthTokens(tokens.accessToken, tokens.refreshToken, (tokens.expiresAt - Date.now()) / 1000);
}
} catch (e) {
console.error("Failed to load Qwen tokens:", e);
}
}
}
console.log("[Home] Initializing Qwen OAuth service on client...");
modelAdapter["qwenService"]["initialize"]?.();
}, []);
const renderContent = () => {
@@ -70,6 +31,16 @@ export default function Home() {
return <PRDGenerator />;
case "action":
return <ActionPlanGenerator />;
case "uxdesigner":
return <UXDesignerPrompt />;
case "slides":
return <SlidesGenerator />;
case "googleads":
return <GoogleAdsGenerator />;
case "market-research":
return <MarketResearcher />;
case "ai-assist":
return <AIAssist />;
case "history":
return <HistoryPanel />;
case "settings":
@@ -82,7 +53,7 @@ export default function Home() {
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">
<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">
{renderContent()}
</div>
@@ -90,3 +61,4 @@ export default function Home() {
</div>
);
}

808
components/AIAssist.tsx Normal file
View File

@@ -0,0 +1,808 @@
"use client";
import React, { useState, useEffect, useRef, memo } from "react";
import {
MessageSquare, Send, Code2, Palette, Search,
Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost,
Wand2, LayoutPanelLeft, Play, Orbit
} from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeHighlight from "rehype-highlight";
import { cn } from "@/lib/utils";
import { AIAssistMessage } from "@/types";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import useStore from "@/lib/store";
import { translations } from "@/lib/i18n/translations";
import modelAdapter from "@/lib/services/adapter-instance";
import { safeJsonFetch } from "@/lib/safeJsonFetch";
// --- Types ---
interface PreviewData {
type: string;
data: string;
language?: string;
isStreaming?: boolean;
}
// --- Specialized Components ---
/**
* A ultra-stable iframe wrapper that avoids hydration issues
* and provides a WOW visual experience.
*/
const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: string, isStreaming: boolean }) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
if (!iframeRef.current || !data) return;
// Decode HTML entities if present
const isEncodedHtml = data.includes("&lt;") && data.includes("&gt;");
const normalized = isEncodedHtml
? data
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, "\"")
.replace(/&#39;/g, "'")
: data;
// Check if the content is a full HTML document or a fragment
const trimmed = normalized.trim();
const isFullDocument = /^<!DOCTYPE/i.test(trimmed) || /^<html/i.test(trimmed);
const hasHeadTag = /<head[\s>]/i.test(normalized);
let doc: string;
if (isFullDocument) {
// If it's a full document, inject Tailwind CSS but keep the structure
if (hasHeadTag) {
doc = normalized.replace(/<head>/i, `<head>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
`);
} else {
doc = normalized.replace(/<html[^>]*>/i, (match) => `${match}
<head>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
</head>
`);
}
} else {
// Wrap fragments in a styled container
doc = `
<!DOCTYPE html>
<html class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: { 50: '#ecfdf3', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' }
}
}
}
}
</script>
<style>
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #115e59; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #0f766e; }
body {
margin: 0;
padding: 24px;
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
background: #f8fafc;
color: #1e293b;
min-height: 100vh;
}
</style>
</head>
<body>
${normalized}
</body>
</html>
`;
}
iframeRef.current.srcdoc = doc;
}, [data, type]);
return (
<div className="w-full h-full relative group">
<iframe
ref={iframeRef}
title="Canvas Preview"
className="w-full h-full border-none rounded-b-2xl bg-[#0b1414] shadow-inner"
sandbox="allow-scripts"
/>
{isStreaming && (
<div className="absolute inset-x-0 bottom-0 h-1 bg-blue-500/20 overflow-hidden">
<div className="h-full bg-blue-500 animate-[loading_1.5s_infinite]" />
</div>
)}
<style jsx>{`
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
`}</style>
</div>
);
});
LiveCanvas.displayName = "LiveCanvas";
// --- Helper Functions ---
function parseStreamingContent(text: string) {
let agent = "general";
let preview: PreviewData | null = null;
let chatDisplay = text.trim();
const decodeHtml = (value: string) => value
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, "\"")
.replace(/&#39;/g, "'");
const stripFences = (value: string) => {
const fenced = value.match(/```(?:html|css|javascript|tsx|jsx|md|markdown)?\s*([\s\S]*?)```/i);
return fenced ? fenced[1].trim() : value.trim();
};
const jsonCandidate = text.trim();
if (jsonCandidate.startsWith("{") && jsonCandidate.endsWith("}")) {
try {
const parsed = JSON.parse(jsonCandidate);
if (parsed?.agent) agent = parsed.agent;
if (parsed?.preview?.data) {
preview = {
type: parsed.preview.type || "web",
language: parsed.preview.language || "text",
data: parsed.preview.data,
isStreaming: !text.includes("[/PREVIEW]")
};
}
if (typeof parsed?.content === "string") {
chatDisplay = parsed.content.trim();
}
} catch {
// Ignore malformed JSON during stream
}
}
const agentMatch = text.match(/\[AGENT:([\w-]+)\]/);
if (agentMatch) agent = agentMatch[1];
const previewMatch = text.match(/\[PREVIEW:([\w-]+):?([\w-]+)?\]([\s\S]*?)(?:\[\/PREVIEW\]|$)/);
if (previewMatch) {
preview = {
type: previewMatch[1],
language: previewMatch[2] || "text",
data: previewMatch[3].trim(),
isStreaming: !text.includes("[/PREVIEW]")
};
}
if (/\[AGENT:|\[PREVIEW:/.test(text)) {
chatDisplay = text
.replace(/\[AGENT:[\w-]+\]/g, "")
.replace(/\[PREVIEW:[\w-]+:?[\w-]+?\][\s\S]*?(?:\[\/PREVIEW\]|$)/g, "")
.trim();
}
if (!preview) {
const fenced = text.match(/```(html|css|javascript|tsx|jsx|md|markdown)\s*([\s\S]*?)```/i);
if (fenced) {
const language = fenced[1].toLowerCase();
preview = {
type: language === "html" ? "web" : "code",
language,
data: fenced[2].trim(),
isStreaming: false
};
}
}
if (preview) {
const htmlSignal = preview.data.toLowerCase().includes("<!doctype") || preview.data.toLowerCase().includes("<html");
const isHtmlLike = ["web", "app", "design", "html", "ui"].includes(preview.type) || preview.language === "html" || htmlSignal;
if (htmlSignal && preview.type === "code") {
preview.type = "web";
}
if (isHtmlLike) {
preview.data = decodeHtml(stripFences(preview.data));
}
}
if (!preview) {
const htmlDoc = text.match(/<!doctype\s+html[\s\S]*$/i) || text.match(/<html[\s\S]*$/i);
if (htmlDoc) {
preview = {
type: "web",
language: "html",
data: decodeHtml(stripFences(htmlDoc[0])),
isStreaming: false
};
}
}
if (!chatDisplay && preview) {
chatDisplay = `Rendering live artifact...`;
}
return { chatDisplay, preview, agent };
}
// --- Main Component ---
export default function AIAssist() {
const {
language,
aiAssistHistory,
setAIAssistHistory,
selectedProvider,
selectedModels,
setSelectedModel
} = useStore();
const t = translations[language].aiAssist;
const [input, setInput] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [currentAgent, setCurrentAgent] = useState("general");
const [previewData, setPreviewData] = useState<PreviewData | null>(null);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [showCanvas, setShowCanvas] = useState(false);
const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
const [abortController, setAbortController] = useState<AbortController | null>(null);
// Agentic States
const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle");
const [aiPlan, setAiPlan] = useState<any>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const isPreviewRenderable = (preview?: PreviewData | null) => {
if (!preview) return false;
return ["web", "app", "design", "html", "ui"].includes(preview.type)
|| preview.language === "html"
|| preview.data.includes("<")
|| (preview.data.includes("&lt;") && preview.data.includes("&gt;"));
};
const canRenderPreview = isPreviewRenderable(previewData);
// Auto-scroll logic
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: 'smooth'
});
}
}, [aiAssistHistory, isProcessing]);
useEffect(() => {
if (previewData?.data) {
// Always default to preview mode - the LiveCanvas will render any content
setViewMode("preview");
}
}, [previewData?.data]);
// Load available models
useEffect(() => {
const loadModels = async () => {
const response = await modelAdapter.listModels(selectedProvider);
if (response.success && response.data) {
const models = response.data[selectedProvider] || [];
setAvailableModels(models);
if (models.length > 0 && !selectedModels[selectedProvider]) {
setSelectedModel(selectedProvider, models[0]);
}
}
};
loadModels();
}, [selectedProvider, selectedModels, setSelectedModel]);
const handleSendMessage = async (e?: React.FormEvent, forcedPrompt?: string) => {
if (e) e.preventDefault();
const finalInput = forcedPrompt || input;
if (!finalInput.trim() || isProcessing) return;
const controller = new AbortController();
setAbortController(controller);
// UI Update for user message
if (!forcedPrompt) {
const userMsg: AIAssistMessage = {
role: "user",
content: finalInput,
timestamp: new Date(),
};
const newHistory = [...aiAssistHistory, userMsg];
setAIAssistHistory(newHistory);
setInput("");
}
setIsProcessing(true);
if (assistStep === "idle") setAssistStep("plan");
const assistantMsg: AIAssistMessage = {
role: "assistant",
content: "",
agent: currentAgent,
timestamp: new Date()
};
setAIAssistHistory(prev => [...prev, assistantMsg]);
try {
// First, get the plan orchestrator prompt from our new API
type AiAssistApiResponse = {
prompt?: string;
step?: string;
requestId?: string;
success?: boolean;
error?: string;
};
const apiResult = await safeJsonFetch<AiAssistApiResponse>("/api/ai-assist", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
request: finalInput,
step: assistStep === "plan" ? "generate" : "plan",
plan: aiPlan
}),
});
if (!apiResult.ok) {
console.error("AI Assist API failed:", apiResult.error);
throw new Error(apiResult.error.message);
}
if (apiResult.data.error) {
throw new Error(apiResult.data.error);
}
const prompt = apiResult.data.prompt ?? "";
let accumulated = "";
let lastParsedPreview: PreviewData | null = null;
const response = await modelAdapter.generateAIAssistStream(
{
messages: [...aiAssistHistory, { role: "system", content: prompt } as any],
currentAgent,
onChunk: (chunk) => {
accumulated += chunk;
const { chatDisplay, preview, agent } = parseStreamingContent(accumulated);
// If we're in planning mode and see JSON, try to parse the plan
if (assistStep === "plan" || assistStep === "idle") {
const jsonMatch = accumulated.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
const parsed = JSON.parse(jsonMatch[0]);
if (parsed.summary && parsed.files) setAiPlan(parsed);
} catch (e) { }
}
}
if (preview && JSON.stringify(preview) !== JSON.stringify(lastParsedPreview)) {
setPreviewData(preview);
lastParsedPreview = preview;
setShowCanvas(true);
}
if (agent !== currentAgent) setCurrentAgent(agent);
setAIAssistHistory(prev => {
const last = prev[prev.length - 1];
if (last && last.role === "assistant") {
return [...prev.slice(0, -1), {
...last,
content: chatDisplay || accumulated,
agent,
preview: preview ? { type: preview.type, data: preview.data, language: preview.language } : undefined
} as AIAssistMessage];
}
return prev;
});
},
signal: controller.signal
},
selectedProvider,
selectedModels[selectedProvider]
);
if (!response.success) throw new Error(response.error);
if (assistStep === "plan" || assistStep === "idle") {
setAssistStep("plan");
} else {
setAssistStep("preview");
}
} catch (error) {
console.error("Assist error:", error);
setAIAssistHistory(prev => {
const last = prev[prev.length - 1];
const message = error instanceof Error ? error.message : "AI Assist failed";
if (last && last.role === "assistant") {
return [...prev.slice(0, -1), { ...last, content: message }];
}
return [...prev, { role: "assistant", content: message, timestamp: new Date() }];
});
} finally {
setIsProcessing(false);
setAbortController(null);
}
};
const approveAndGenerate = () => {
setAssistStep("generating");
handleSendMessage(undefined, "Approved. Please generate the code according to the plan.");
};
const stopGeneration = () => {
if (abortController) {
abortController.abort();
setAbortController(null);
setIsProcessing(false);
}
};
const clearHistory = () => {
setAIAssistHistory([]);
setPreviewData(null);
setShowCanvas(false);
setAssistStep("idle");
setAiPlan(null);
};
return (
<div className="ai-assist h-[calc(100vh-140px)] flex flex-col lg:flex-row gap-4 lg:gap-8 overflow-hidden animate-in fade-in duration-700">
{/* --- Chat Panel --- */}
<div className={cn(
"flex flex-col h-full transition-all duration-700 cubic-bezier(0.4, 0, 0.2, 1)",
showCanvas ? "w-full lg:w-2/5 lg:min-w-[400px]" : "w-full max-w-4xl mx-auto"
)}>
<Card className="flex-1 flex flex-col border border-blue-100/60 dark:border-blue-950/60 shadow-[0_18px_50px_rgba(15,23,42,0.15)] bg-[#f8f5ef]/80 dark:bg-[#0b1414]/80 backdrop-blur-2xl rounded-[2rem] overflow-hidden">
{/* Header */}
<div className="px-6 py-5 border-b border-blue-100/60 dark:border-blue-950/40 flex items-center justify-between shrink-0 bg-white/60 dark:bg-[#0b1414]/60 backdrop-blur-md">
<div className="flex items-center gap-4">
<div className="relative">
<div className="p-2.5 bg-gradient-to-tr from-blue-500 to-teal-600 rounded-2xl text-white shadow-lg shadow-blue-500/20">
<MessageSquare className="h-5 w-5" />
</div>
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-amber-400 border-2 border-white dark:border-[#0b1414] animate-pulse" />
</div>
<div>
<h2 className="text-xl font-black text-slate-900 dark:text-blue-50 tracking-tight">{t.title}</h2>
<p className="text-[11px] font-bold uppercase tracking-[0.25em] text-blue-700/70 dark:text-blue-200/70">
Agent {currentAgent}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<select
value={selectedModels[selectedProvider]}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="text-[11px] font-black h-9 px-3 rounded-xl border-blue-100 dark:border-blue-900 bg-white/80 dark:bg-[#0b1414]/80 focus:ring-2 focus:ring-blue-400/40 transition-all outline-none"
>
{availableModels.map(m => <option key={m} value={m}>{m}</option>)}
</select>
<Button
variant="ghost"
size="icon"
onClick={() => setShowCanvas((prev) => !prev)}
className="h-9 w-9 text-blue-700 hover:text-blue-950 hover:bg-blue-100 dark:text-blue-200 dark:hover:text-white dark:hover:bg-blue-900/40 rounded-xl transition-colors"
disabled={!previewData}
>
<LayoutPanelLeft className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={clearHistory}
className="h-9 w-9 text-slate-400 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-900/20 rounded-xl transition-colors"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
{/* Messages */}
<div className="px-6 pt-6">
<div className="flex flex-wrap gap-2 pb-4">
{[
{ label: "General", agent: "general", icon: <Orbit className="h-3.5 w-3.5" /> },
{ label: "Code", agent: "code", icon: <Code2 className="h-3.5 w-3.5" /> },
{ label: "Design", agent: "design", icon: <Palette className="h-3.5 w-3.5" /> },
{ label: "SEO", agent: "seo", icon: <Search className="h-3.5 w-3.5" /> },
{ label: "Web", agent: "web", icon: <LayoutPanelLeft className="h-3.5 w-3.5" /> },
{ label: "App", agent: "app", icon: <Play className="h-3.5 w-3.5" /> },
].map(({ label, agent, icon }) => (
<button
key={agent}
onClick={() => setCurrentAgent(agent)}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-full text-[11px] font-black uppercase tracking-widest border transition-all",
currentAgent === agent
? "bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-600/30"
: "bg-white/70 text-blue-700 border-blue-100 hover:border-blue-300 dark:bg-[#0f1a1a] dark:text-blue-200 dark:border-blue-900"
)}
>
{icon}
{label}
</button>
))}
</div>
</div>
<div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-6 space-y-8 scrollbar-thin scrollbar-thumb-blue-200/60 dark:scrollbar-thumb-blue-900">
{aiAssistHistory.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-center py-20 animate-in zoom-in-95 duration-500">
<div className="p-8 bg-blue-500/5 dark:bg-blue-500/10 rounded-full mb-8 relative">
<Ghost className="h-20 w-20 text-blue-400/40 animate-bounce duration-[3s]" />
<div className="absolute inset-0 bg-blue-500/10 blur-3xl rounded-full" />
</div>
<h3 className="text-3xl font-black text-slate-900 dark:text-blue-50 mb-3 tracking-tighter">Studio-grade AI Assist</h3>
<p className="max-w-xs text-sm font-medium text-slate-600 dark:text-blue-100/70 leading-relaxed">
Switch agents, stream answers, and light up the canvas with live artifacts.
</p>
<div className="mt-10 flex flex-wrap justify-center gap-3">
{[
{ label: "Build a landing UI", agent: "web" },
{ label: "SEO diagnostic", agent: "seo" },
{ label: "Mobile onboarding", agent: "app" },
].map((chip) => (
<Badge
key={chip.label}
variant="secondary"
className="px-4 py-2 rounded-full cursor-pointer hover:bg-blue-600 hover:text-white transition-all text-[11px] font-black border-transparent shadow-sm"
onClick={() => {
setCurrentAgent(chip.agent);
setInput(chip.label);
}}
>
{chip.label}
</Badge>
))}
</div>
</div>
)}
{aiAssistHistory.map((msg, i) => (
<div key={i} className={cn(
"flex flex-col gap-3 group animate-in slide-in-from-bottom-4 duration-500",
msg.role === "user" ? "items-end" : "items-start"
)}>
<div className={cn(
"max-w-[90%] p-5 rounded-3xl relative transition-all duration-300",
msg.role === "user"
? "bg-gradient-to-br from-blue-600 to-teal-600 text-white rounded-tr-none shadow-[0_10px_24px_rgba(16,185,129,0.25)]"
: "bg-white dark:bg-[#0f1a1a]/80 border border-blue-100/70 dark:border-blue-900/50 text-slate-700 dark:text-blue-50 rounded-tl-none shadow-sm backdrop-blur-xl"
)}>
<div className="absolute top-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => navigator.clipboard.writeText(msg.content)} className="text-inherit opacity-40 hover:opacity-100">
<Copy className="h-3 w-3" />
</button>
</div>
<div className="prose prose-sm dark:prose-invert max-w-none leading-relaxed font-medium">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
{msg.content || (msg.role === "assistant" ? "..." : "")}
</ReactMarkdown>
</div>
{/* Agentic Plan Review Card */}
{msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && (
<div className="mt-6 p-6 rounded-2xl bg-blue-500/5 border border-blue-500/20 backdrop-blur-sm animate-in zoom-in-95 duration-300">
<h3 className="text-sm font-black text-blue-400 uppercase tracking-widest mb-4 flex items-center gap-2">
<LayoutPanelLeft className="h-4 w-4" /> Proposed Solution Plan
</h3>
<div className="space-y-4">
<div>
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Architecture</p>
<p className="text-xs text-slate-400">{aiPlan.architecture}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Tech Stack</p>
<div className="flex flex-wrap gap-1">
{aiPlan.techStack?.map((t: string) => (
<Badge key={t} variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t}</Badge>
))}
</div>
</div>
<div>
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Files</p>
<p className="text-[10px] text-slate-400">{aiPlan.files?.length} modules planned</p>
</div>
</div>
<Button
onClick={approveAndGenerate}
disabled={isProcessing}
className="w-full mt-4 bg-blue-600 hover:bg-blue-500 text-white font-black uppercase text-[10px] tracking-widest py-5 rounded-xl shadow-lg shadow-blue-500/20"
>
{isProcessing ? "Starting Engine..." : "Approve & Generate Development"}
</Button>
</div>
</div>
)}
{msg.role === "assistant" && msg.preview && (
<Button
variant="secondary"
size="sm"
className="mt-5 w-full bg-blue-50 dark:bg-blue-900/30 border border-blue-200/60 dark:border-blue-800 text-blue-700 dark:text-blue-200 font-black uppercase tracking-[0.1em] text-[10px] rounded-2xl h-11 hover:scale-[1.02] active:scale-[0.98] transition-all"
onClick={() => {
const nextPreview = { ...msg.preview!, isStreaming: false };
setPreviewData(nextPreview);
setViewMode(isPreviewRenderable(nextPreview) ? "preview" : "code");
setShowCanvas(true);
}}
>
<Zap className="h-3.5 w-3.5 mr-2" /> Activate Artifact
</Button>
)}
</div>
<div className="flex items-center gap-2 px-2">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">
{msg.role === "assistant" ? `Agent ${msg.agent || 'core'}` : 'Explorer'}
</span>
</div>
</div>
))}
</div>
{/* Input Area */}
<div className="p-6 bg-white/70 dark:bg-[#0b1414]/60 border-t border-blue-100/60 dark:border-blue-950/40 shrink-0">
<form onSubmit={handleSendMessage} className="relative group">
<div className="absolute inset-0 bg-blue-500/5 rounded-[1.5rem] blur-xl group-focus-within:bg-blue-500/10 transition-all" />
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={t.placeholder}
disabled={isProcessing}
className="relative pr-24 py-7 rounded-[1.5rem] bg-white/90 dark:bg-[#0f1a1a]/70 border-blue-200/80 dark:border-blue-900/80 shadow-lg shadow-blue-500/5 focus:ring-4 focus:ring-blue-500/10 transition-all font-medium text-base h-16 outline-none"
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
{isProcessing ? (
<Button
type="button"
variant="ghost"
onClick={stopGeneration}
className="h-10 w-10 p-0 rounded-2xl bg-rose-500/10 text-rose-500 hover:bg-rose-500 hover:text-white animate-in zoom-in-75 transition-all"
>
<StopCircle className="h-5 w-5" />
</Button>
) : (
<Button
type="submit"
disabled={!input.trim()}
className="h-11 w-11 rounded-2xl bg-blue-600 shadow-lg shadow-blue-600/30 hover:scale-105 active:scale-95 transition-all p-0"
>
<Send className="h-5 w-5" />
</Button>
)}
</div>
</form>
<div className="flex items-center justify-between mt-4 text-[11px] font-semibold text-blue-700/70 dark:text-blue-100/70">
<span className="flex items-center gap-2">
<Wand2 className="h-3.5 w-3.5" />
Ask for a design, code, or research artifact.
</span>
<span className="flex items-center gap-2">
<LayoutPanelLeft className="h-3.5 w-3.5" />
Canvas {previewData ? "ready" : "idle"}
</span>
</div>
</div>
</Card>
</div>
{/* --- Canvas Panel --- */}
{
showCanvas && (
<div className="flex-1 h-full min-w-0 animate-in slide-in-from-right-12 duration-700 cubic-bezier(0,0,0.2,1)">
<Card className="h-full flex flex-col bg-[#081010] rounded-[2.5rem] overflow-hidden border border-blue-900/60 shadow-[0_20px_80px_rgba(0,0,0,0.6)]">
<div className="px-6 py-5 border-b border-blue-900/60 bg-[#0b1414]/70 backdrop-blur-2xl flex items-center justify-between shrink-0">
<div className="flex items-center gap-4">
<div className="p-2.5 bg-blue-500/10 rounded-2xl border border-blue-500/20">
{viewMode === "preview" ? <Monitor className="h-5 w-5 text-blue-400" /> : <Code2 className="h-5 w-5 text-amber-300" />}
</div>
<div>
<h3 className="text-xs font-black text-blue-50 uppercase tracking-[0.2em]">{previewData?.type || "Live"} Canvas</h3>
<div className="flex bg-blue-900/60 rounded-xl p-1 mt-2">
<button
onClick={() => setViewMode("preview")}
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "preview" ? "bg-blue-500 text-white shadow-lg" : "text-blue-300/60 hover:text-blue-100")}
>
Live Render
</button>
<button
onClick={() => setViewMode("code")}
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "code" ? "bg-blue-500 text-white shadow-lg" : "text-blue-300/60 hover:text-blue-100")}
>
Inspect Code
</button>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
className="h-10 w-10 text-blue-200/70 hover:text-white hover:bg-blue-900 rounded-2xl"
onClick={() => navigator.clipboard.writeText(previewData?.data || "")}
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-10 w-10 text-blue-200/70 hover:text-rose-400 hover:bg-rose-500/10 rounded-2xl"
onClick={() => setShowCanvas(false)}
>
<X className="h-5 w-5" />
</Button>
</div>
</div>
<div className="flex-1 overflow-hidden relative">
{viewMode === "preview" && previewData ? (
<LiveCanvas
data={previewData.data}
type={previewData.type}
isStreaming={!!previewData.isStreaming}
/>
) : (
<div className="h-full bg-[#050505] p-8 font-mono text-sm overflow-auto scrollbar-thin scrollbar-thumb-blue-900">
<pre className="text-blue-300/90 leading-relaxed selection:bg-blue-500/20 whitespace-pre-wrap">
<code>{previewData?.data}</code>
</pre>
</div>
)}
</div>
<div className="px-6 py-3 border-t border-blue-900/40 bg-[#0b1414]/70 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={cn("w-2 h-2 rounded-full", previewData?.isStreaming ? "bg-amber-500 animate-pulse" : "bg-blue-500")} />
<span className="text-[10px] text-blue-200/60 font-bold uppercase tracking-widest leading-none">
{previewData?.isStreaming ? "Neural Link Active" : "Sync Complete"}
</span>
</div>
<Badge variant="outline" className="text-[9px] border-blue-900 text-blue-200/50 font-black">
{previewData?.language?.toUpperCase()} UTF-8
</Badge>
</div>
</Card>
</div>
)
}
<style jsx global>{`
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap');
.ai-assist {
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
}
.ai-assist .prose :where(code):not(:where([class~="not-prose"] *)) {
color: inherit;
}
`}</style>
</div >
);
}

View File

@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function ActionPlanGenerator() {
const {
language,
currentPrompt,
actionPlan,
selectedProvider,
@@ -28,6 +30,9 @@ export default function ActionPlanGenerator() {
setSelectedModel,
} = useStore();
const t = translations[language].actionPlan;
const common = translations[language].common;
const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider];
@@ -71,7 +76,9 @@ export default function ActionPlanGenerator() {
}
const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) {
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
@@ -79,9 +86,13 @@ export default function ActionPlanGenerator() {
setProcessing(true);
setError(null);
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
console.log("[ActionPlanGenerator] Generation result:", result);
if (result.success && result.data) {
const newPlan = {
id: Math.random().toString(36).substr(2, 9),
@@ -100,9 +111,11 @@ export default function ActionPlanGenerator() {
};
setActionPlan(newPlan);
} else {
console.error("[ActionPlanGenerator] Generation failed:", result.error);
setError(result.error || "Failed to generate action plan");
}
} catch (err) {
console.error("[ActionPlanGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
@@ -118,28 +131,28 @@ export default function ActionPlanGenerator() {
};
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">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ListTodo className="h-5 w-5" />
Action Plan Generator
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription>
Convert PRD into actionable implementation plan
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label>
<div className="flex gap-2">
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
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"}
</Button>
@@ -147,12 +160,12 @@ export default function ActionPlanGenerator() {
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Model</label>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
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) => (
<option key={model} value={model}>
@@ -163,91 +176,91 @@ export default function ActionPlanGenerator() {
</div>
<div className="space-y-2">
<label className="text-sm font-medium">PRD / Requirements</label>
<label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
<Textarea
placeholder="Paste your PRD or project requirements here..."
placeholder={t.placeholder}
value={currentPrompt}
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>
{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}
{!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="text-xs">Configure API key in Settings</span>
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</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 ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating Action Plan...
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
{common.generating}
</>
) : (
<>
<ListTodo className="mr-2 h-4 w-4" />
Generate Action Plan
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
</>
)}
</Button>
</CardContent>
</Card>
<Card className={cn(!actionPlan && "opacity-50")}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
Action Plan
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
{t.generatedTitle}
</span>
{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 ? (
<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>
)}
</CardTitle>
<CardDescription>
Task breakdown, frameworks, and architecture recommendations
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{actionPlan ? (
<div className="space-y-4">
<div className="rounded-md border bg-primary/5 p-4">
<h4 className="mb-2 flex items-center gap-2 font-semibold">
<Clock className="h-4 w-4" />
Implementation Roadmap
<div className="space-y-3 lg:space-y-4">
<div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
{language === "ru" ? "Дорожная карта реализации" : language === "he" ? "מפת דרכים ליישום" : "Implementation Roadmap"}
</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 className="rounded-md border bg-muted/30 p-4">
<h4 className="mb-2 flex items-center gap-2 font-semibold">
<AlertTriangle className="h-4 w-4" />
Quick Notes
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
{language === "ru" ? "Быстрые заметки" : language === "he" ? "הערות מהירות" : "Quick Notes"}
</h4>
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
<li>Review all task dependencies before starting</li>
<li>Set up recommended framework architecture</li>
<li>Follow best practices for security and performance</li>
<li>Use specified deployment strategy</li>
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
<li>{language === "ru" ? "Проверьте все зависимости задач перед началом" : language === "he" ? "בדוק את כל התלות בין המשימות לפני שתתחיל" : "Review all task dependencies before starting"}</li>
<li>{language === "ru" ? "Настройте рекомендуемую архитектуру фреймворка" : language === "he" ? "הגדר את ארכיטקטורת הפרימוורק המומלצת" : "Set up recommended framework architecture"}</li>
<li>{language === "ru" ? "Следуйте лучшим практикам безопасности и производительности" : language === "he" ? "עקוב אחר שיטות עבודה מומלצות לאבטחה וביצועים" : "Follow best practices for security and performance"}</li>
<li>{language === "ru" ? "Используйте указанную стратегию развертывания" : language === "he" ? "השתמש באסטרטגיית הפריסה המצוינת" : "Use specified deployment strategy"}</li>
</ul>
</div>
</div>
) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground">
Action plan will appear here
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
{t.emptyState}
</div>
)}
</CardContent>

View File

@@ -0,0 +1,57 @@
"use client";
import React from "react";
import { AlertTriangle, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
interface Props {
children: React.ReactNode;
fallback?: React.ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
resetError = () => {
this.setState({ hasError: false, error: undefined });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<div className="flex flex-col items-center justify-center p-8 bg-slate-50 border border-slate-200 rounded-2xl h-full text-center">
<div className="bg-rose-100 p-3 rounded-full mb-4">
<AlertTriangle className="h-6 w-6 text-rose-600" />
</div>
<h3 className="text-lg font-bold text-slate-900 mb-2">Something went wrong</h3>
<p className="text-sm text-slate-500 max-w-xs mb-6">
{this.state.error?.message || "An unexpected error occurred while rendering this component."}
</p>
<Button onClick={this.resetError} variant="outline">
<RotateCcw className="h-4 w-4 mr-2" /> Try Again
</Button>
</div>
);
}
return this.props.children;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,17 +4,20 @@ import useStore from "@/lib/store";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Clock, Trash2, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function HistoryPanel() {
const { history, setCurrentPrompt, clearHistory } = useStore();
const { language, history, setCurrentPrompt, clearHistory } = useStore();
const t = translations[language].history;
const common = translations[language].common;
const handleRestore = (prompt: string) => {
setCurrentPrompt(prompt);
};
const handleClear = () => {
if (confirm("Are you sure you want to clear all history?")) {
const message = language === "ru" ? "Вы уверены, что хотите очистить всю историю?" : language === "he" ? "האם אתה בטוח שברצונך למחוק את כל ההיסטוריה?" : "Are you sure you want to clear all history?";
if (confirm(message)) {
clearHistory();
}
};
@@ -22,12 +25,12 @@ export default function HistoryPanel() {
if (history.length === 0) {
return (
<Card>
<CardContent className="flex h-[400px] items-center justify-center">
<div className="text-center">
<Clock className="mx-auto h-12 w-12 text-muted-foreground/50" />
<p className="mt-4 text-muted-foreground">No history yet</p>
<p className="mt-2 text-sm text-muted-foreground">
Start enhancing prompts to see them here
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6 text-center">
<div>
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground font-medium">{t.empty}</p>
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
{language === "ru" ? "Начните использовать инструменты, чтобы увидеть историю здесь" : language === "he" ? "התחל להשתמש בכלים כדי לראות אותם כאן" : "Start using tools to see them here"}
</p>
</div>
</CardContent>
@@ -37,35 +40,37 @@ export default function HistoryPanel() {
return (
<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>
<CardTitle>History</CardTitle>
<CardDescription>{history.length} items</CardDescription>
<CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{history.length} {language === "ru" ? "элем." : language === "he" ? "פריטים" : "items"}
</CardDescription>
</div>
<Button variant="outline" size="icon" onClick={handleClear}>
<Trash2 className="h-4 w-4" />
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9" title={t.clear}>
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</Button>
</CardHeader>
<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) => (
<div
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">
<span className="text-xs text-muted-foreground">
<div className="mb-1.5 lg:mb-2 flex items-center justify-between gap-2">
<span className="text-[10px] lg:text-xs text-muted-foreground truncate">
{new Date(item.timestamp).toLocaleString()}
</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="h-6 w-6 flex-shrink-0"
onClick={() => handleRestore(item.prompt)}
>
<RotateCcw className="h-3 w-3" />
</Button>
</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>
))}
</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 "Invalid primary website URL";
const validCompetitors = competitorUrls.filter(url => url.trim().length > 0);
if (validCompetitors.length < 2) return "At least 2 competitor websites are required";
for (const url of validCompetitors) {
if (!urlRegex.test(url)) return `Invalid competitor URL: ${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(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
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 || "Main Product", features: [] }],
generatedAt: new Date(),
rawContent: result.data
});
} catch (e) {
console.error("Failed to parse market research JSON:", e);
setError("Failed to parse the AI response. Please try again.");
}
} else {
setError(result.error || "Research failed");
}
} catch (err) {
setError(err instanceof Error ? err.message : "An unexpected error occurred");
} 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]">Product</th>
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">Your Price</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 : "N/A"}</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" />
View Product
</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]">Feature</th>
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">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>
) : "N/A"}
</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" /> Company Profile
</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" /> Add URL
</Button>
</label>
<div className="space-y-2">
{additionalUrls.map((url, i) => (
<div key={i} className="flex gap-2 group">
<Input
placeholder="Sub-page URL (e.g., pricing, features)"
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" /> Competitive Intel
</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" /> Add Competitor
</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">Describe which products/features to compare across all sites.</p>
</div>
<div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-slate-600">Research Parameters</label>
<Textarea
placeholder="Any specific depth or focus? (e.g., 'Focus on enterprise features', 'Analyze pricing tiers')"
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" /> {language === "ru" ? "Идет анализ" : language === "he" ? "מנתח..." : "Analysis in progress"}
</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" /> AI Thoughts & Actions
</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]">Market Intel Report</Badge>
<CardTitle className="text-2xl font-black tracking-tight">{marketResearchResult.websiteUrl}</CardTitle>
<CardDescription className="text-indigo-200 font-medium">Generated on {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">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">Price Matrix</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">Feature Table</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">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" /> Executive Summary
</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" /> Strategic Advantages
</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" /> Identified Gaps
</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" /> Key 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">Price Comparison Matrix</h3>
<Badge className="bg-slate-900 text-[10px] font-black uppercase">Live Market Data</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">Feature Benchmarking</h3>
<Badge className="bg-indigo-600 text-[10px] font-black uppercase">Functional Audit</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" /> Market Landscape
</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" /> Segmentation Strategy
</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">Research 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">Awaiting Analysis Parameters</h3>
<p className="text-sm text-slate-400 font-medium max-w-[280px] mt-2 group-hover:text-slate-500 transition-colors">
{t.emptyState}
</p>
</Card>
)}
</div>
</div>
</div>
);
};
export default MarketResearcher;

View File

@@ -8,6 +8,7 @@ import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function PRDGenerator() {
const {
@@ -19,6 +20,7 @@ export default function PRDGenerator() {
apiKeys,
isProcessing,
error,
language,
setCurrentPrompt,
setSelectedProvider,
setPRD,
@@ -28,6 +30,9 @@ export default function PRDGenerator() {
setSelectedModel,
} = useStore();
const t = translations[language].prdGenerator;
const common = translations[language].common;
const [copied, setCopied] = useState(false);
const [expandedSections, setExpandedSections] = useState<string[]>([]);
@@ -78,7 +83,9 @@ export default function PRDGenerator() {
}
const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) {
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
@@ -86,9 +93,13 @@ export default function PRDGenerator() {
setProcessing(true);
setError(null);
console.log("[PRDGenerator] Starting PRD generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel);
console.log("[PRDGenerator] Generation result:", result);
if (result.success && result.data) {
const newPRD = {
id: Math.random().toString(36).substr(2, 9),
@@ -105,9 +116,11 @@ export default function PRDGenerator() {
};
setPRD(newPRD);
} else {
console.error("[PRDGenerator] Generation failed:", result.error);
setError(result.error || "Failed to generate PRD");
}
} catch (err) {
console.error("[PRDGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
@@ -123,37 +136,37 @@ export default function PRDGenerator() {
};
const sections = [
{ id: "overview", title: "Overview & Objectives" },
{ id: "personas", title: "User Personas & Use Cases" },
{ id: "functional", title: "Functional Requirements" },
{ id: "nonfunctional", title: "Non-functional Requirements" },
{ id: "architecture", title: "Technical Architecture" },
{ id: "metrics", title: "Success Metrics" },
{ id: "overview", title: language === "ru" ? "Обзор продукта" : language === "he" ? "סקירת מוצר" : "Product Overview" },
{ id: "personas", title: language === "ru" ? "Персоны пользователей" : language === "he" ? "פרסונות משתמשים" : "User Personas & Use Cases" },
{ id: "functional", title: language === "ru" ? "Функциональные требования" : language === "he" ? "דרישות פונקציונליות" : "Functional Requirements" },
{ id: "nonfunctional", title: language === "ru" ? "Нефункциональные требования" : language === "he" ? "דרישות לא פונקציונליות" : "Non-functional Requirements" },
{ id: "architecture", title: language === "ru" ? "Техническая архитектура" : language === "he" ? "ארכיטקטורה טכנית" : "Technical Architecture" },
{ id: "metrics", title: language === "ru" ? "Успешность метрик" : language === "he" ? "מדדי הצלחה" : "Success Metrics" },
];
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">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
PRD Generator
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription>
Generate comprehensive Product Requirements Document from your idea
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label>
<div className="flex gap-2">
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
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"}
</Button>
@@ -161,12 +174,12 @@ export default function PRDGenerator() {
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Model</label>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
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) => (
<option key={model} value={model}>
@@ -177,37 +190,36 @@ export default function PRDGenerator() {
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Your Idea</label>
<Textarea
placeholder="e.g., A task management app with real-time collaboration features"
placeholder={t.placeholder}
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y"
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/>
</div>
{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}
{!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="text-xs">Configure API key in Settings</span>
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</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 ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating PRD...
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
{common.generating}
</>
) : (
<>
<FileText className="mr-2 h-4 w-4" />
Generate PRD
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
{common.generate}
</>
)}
</Button>
@@ -215,53 +227,53 @@ export default function PRDGenerator() {
</Card>
<Card className={cn(!prd && "opacity-50")}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
Generated PRD
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
{t.generatedTitle}
</span>
{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 ? (
<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>
)}
</CardTitle>
<CardDescription>
Structured requirements document ready for development
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Структурированный документ требований готов к разработке" : language === "he" ? "מסמך דרישות מובנה מוכן לפיתוח" : "Structured requirements document ready for development"}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{prd ? (
<div className="space-y-3">
<div className="space-y-2 lg:space-y-3">
{sections.map((section) => (
<div key={section.id} className="rounded-md border bg-muted/30">
<button
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>
{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>
{expandedSections.includes(section.id) && (
<div className="border-t bg-background px-4 py-3">
<pre className="whitespace-pre-wrap text-sm">{prd.overview}</pre>
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3">
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{prd.overview}</pre>
</div>
)}
</div>
))}
</div>
) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground">
PRD will appear here
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
{language === "ru" ? "Здесь появится созданный PRD" : language === "he" ? "PRD שחולל יופיע כאן" : "Generated PRD will appear here"}
</div>
)}
</CardContent>

View File

@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function PromptEnhancer() {
const {
language,
currentPrompt,
enhancedPrompt,
selectedProvider,
@@ -28,6 +30,9 @@ export default function PromptEnhancer() {
setSelectedModel,
} = useStore();
const t = translations[language].promptEnhancer;
const common = translations[language].common;
const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider];
@@ -71,7 +76,9 @@ export default function PromptEnhancer() {
}
const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) {
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
@@ -79,15 +86,21 @@ export default function PromptEnhancer() {
setProcessing(true);
setError(null);
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[PromptEnhancer] Enhancement result:", result);
if (result.success && result.data) {
setEnhancedPrompt(result.data);
} else {
console.error("[PromptEnhancer] Enhancement failed:", result.error);
setError(result.error || "Failed to enhance prompt");
}
} catch (err) {
console.error("[PromptEnhancer] Enhancement error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
@@ -109,21 +122,21 @@ export default function PromptEnhancer() {
};
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">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Input Prompt
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription>
Enter your prompt and we'll enhance it for AI coding agents
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label>
<div className="flex flex-wrap gap-2">
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
@@ -131,7 +144,7 @@ export default function PromptEnhancer() {
size="sm"
onClick={() => setSelectedProvider(provider)}
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"
)}
>
@@ -141,12 +154,12 @@ export default function PromptEnhancer() {
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Model</label>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
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) => (
<option key={model} value={model}>
@@ -156,79 +169,79 @@ export default function PromptEnhancer() {
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Your Prompt</label>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
<Textarea
placeholder="e.g., Create a user authentication system with JWT tokens"
placeholder={t.placeholder}
value={currentPrompt}
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>
{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}
{!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="text-xs">Configure API key in Settings</span>
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div>
)}
</div>
)}
<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 ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enhancing...
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
{common.generating}
</>
) : (
<>
<Sparkles className="mr-2 h-4 w-4" />
Enhance Prompt
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
{t.title}
</>
)}
</Button>
<Button variant="outline" onClick={handleClear} disabled={isProcessing}>
<RefreshCw className="mr-2 h-4 w-4" />
Clear
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
</Button>
</div>
</CardContent>
</Card>
<Card className={cn(!enhancedPrompt && "opacity-50")}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
Enhanced Prompt
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
{t.enhancedTitle}
</span>
{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 ? (
<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>
)}
</CardTitle>
<CardDescription>
Professional prompt ready for coding agents
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{enhancedPrompt ? (
<div className="rounded-md border bg-muted/50 p-4">
<pre className="whitespace-pre-wrap text-sm">{enhancedPrompt}</pre>
<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-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
</div>
) : (
<div className="flex h-[200px] items-center justify-center text-center text-sm text-muted-foreground">
Enhanced prompt will appear here
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
{language === "ru" ? "Улучшенный промпт появится здесь" : language === "he" ? "פרומפט משופר יופיע כאן" : "Enhanced prompt will appear here"}
</div>
)}
</CardContent>

View File

@@ -3,21 +3,23 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function SettingsPanel() {
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
const { language, apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
const t = translations[language].settings;
const common = translations[language].common;
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
const [isAuthLoading, setIsAuthLoading] = useState(false);
const handleSave = () => {
if (typeof window !== "undefined") {
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
alert("API keys saved successfully!");
alert(t.keysSaved);
}
};
@@ -43,6 +45,10 @@ export default function SettingsPanel() {
console.error("Failed to load API keys:", e);
}
}
const storedTokens = modelAdapter.getQwenTokenInfo();
if (storedTokens) {
setQwenTokens(storedTokens);
}
}
};
@@ -62,53 +68,76 @@ export default function SettingsPanel() {
}
};
const handleQwenAuth = async () => {
if (qwenTokens) {
setQwenTokens(null);
modelAdapter.updateQwenTokens();
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
return;
}
setIsAuthLoading(true);
try {
const token = await modelAdapter.startQwenOAuth();
setQwenTokens(token);
modelAdapter.updateQwenTokens(token);
} catch (error) {
console.error("Qwen OAuth failed", error);
window.alert(
error instanceof Error ? error.message : t.qwenAuth + " failed"
);
} finally {
setIsAuthLoading(false);
}
};
useEffect(() => {
handleLoad();
}, []);
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>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
API Configuration
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
{t.apiKeys}
</CardTitle>
<CardDescription>
Configure API keys for different AI providers
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Настройте ключи API для различных провайдеров ИИ" : language === "he" ? "הגדר מפתחות API עבור ספקי בינה מלאכותית שונים" : "Configure API keys for different AI providers"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Server className="h-4 w-4" />
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Qwen Code API Key
</label>
<div className="relative">
<Input
type={showApiKey.qwen ? "text" : "password"}
placeholder="Enter your Qwen API key"
placeholder={t.enterKey("Qwen")}
value={apiKeys.qwen || ""}
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
className="font-mono text-sm"
className="font-mono text-xs lg:text-sm pr-10"
/>
<Button
type="button"
variant="ghost"
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 }))}
>
{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>
</div>
<div className="flex items-center gap-4">
<p className="text-xs text-muted-foreground flex-1">
Get API key from{" "}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
{t.getApiKey}{" "}
<a
href="https://help.aliyun.com/zh/dashscope/"
target="_blank"
@@ -121,56 +150,53 @@ export default function SettingsPanel() {
<Button
variant={qwenTokens ? "secondary" : "outline"}
size="sm"
className="h-8"
onClick={() => {
if (qwenTokens) {
setQwenTokens(undefined as any);
localStorage.removeItem("promptarch-qwen-tokens");
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
} else {
window.location.href = modelAdapter.getQwenAuthUrl();
}
}}
className="h-7 lg:h-8 text-[10px] lg:text-xs w-full sm:w-auto"
onClick={handleQwenAuth}
disabled={isAuthLoading}
>
{qwenTokens ? "Logout from Qwen" : "Login with Qwen (OAuth)"}
{isAuthLoading
? (language === "ru" ? "Вход..." : language === "he" ? "מתחבר..." : "Signing in...")
: qwenTokens
? t.logoutQwen
: t.loginQwen}
</Button>
</div>
{qwenTokens && (
<p className="text-[10px] text-green-600 dark:text-green-400 font-medium">
Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
<p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
{t.authenticated} ({t.expires}: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
</p>
)}
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Server className="h-4 w-4" />
<div className="space-y-2 text-start">
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Ollama Cloud API Key
</label>
<div className="relative">
<Input
type={showApiKey.ollama ? "text" : "password"}
placeholder="Enter your Ollama API key"
placeholder={t.enterKey("Ollama")}
value={apiKeys.ollama || ""}
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
className="font-mono text-sm"
className="font-mono text-xs lg:text-sm pr-10"
/>
<Button
type="button"
variant="ghost"
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 }))}
>
{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>
</div>
<p className="text-xs text-muted-foreground">
Get API key from{" "}
<p className="text-[10px] lg:text-xs text-muted-foreground">
{t.getApiKey}{" "}
<a
href="https://ollama.com/cloud"
target="_blank"
@@ -182,35 +208,35 @@ export default function SettingsPanel() {
</p>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Server className="h-4 w-4" />
<div className="space-y-2 text-start">
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Z.AI Plan API Key
</label>
<div className="relative">
<Input
type={showApiKey.zai ? "text" : "password"}
placeholder="Enter your Z.AI API key"
placeholder={t.enterKey("Z.AI")}
value={apiKeys.zai || ""}
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
className="font-mono text-sm"
className="font-mono text-xs lg:text-sm pr-10"
/>
<Button
type="button"
variant="ghost"
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 }))}
>
{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>
</div>
<p className="text-xs text-muted-foreground">
Get API key from{" "}
<p className="text-[10px] lg:text-xs text-muted-foreground">
{t.getApiKey}{" "}
<a
href="https://docs.z.ai"
target="_blank"
@@ -222,45 +248,44 @@ export default function SettingsPanel() {
</p>
</div>
<Button onClick={handleSave} className="w-full">
<Save className="mr-2 h-4 w-4" />
Save API Keys
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
{t.saveKeys}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Default Provider</CardTitle>
<CardDescription>
Select your preferred AI provider
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="text-base lg:text-lg">{t.defaultProvider}</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.defaultProviderDesc}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3">
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="grid gap-2 lg:gap-3">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<button
key={provider}
onClick={() => setSelectedProvider(provider)}
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 ${
selectedProvider === provider
className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg:p-4 text-left transition-colors hover:bg-muted/50 ${selectedProvider === provider
? "border-primary bg-primary/5"
: "border-border"
}`}
>
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10">
<Server className="h-5 w-5 text-primary" />
<div className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
<Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
</div>
<div className="flex-1">
<h3 className="font-medium capitalize">{provider}</h3>
<p className="text-sm text-muted-foreground">
{provider === "qwen" && "Alibaba DashScope API"}
{provider === "ollama" && "Ollama Cloud API"}
{provider === "zai" && "Z.AI Plan API"}
<div className="flex-1 min-w-0">
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
{provider === "qwen" && t.qwenDesc}
{provider === "ollama" && t.ollamaDesc}
{provider === "zai" && t.zaiDesc}
</p>
</div>
{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>
))}
@@ -269,16 +294,16 @@ export default function SettingsPanel() {
</Card>
<Card>
<CardHeader>
<CardTitle>Data Privacy</CardTitle>
<CardDescription>
Your data handling preferences
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Ваши настройки обработки данных" : language === "he" ? "העדפות הטיפול בנתונים שלך" : "Your data handling preferences"}
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/30 p-4">
<p className="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.
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
<p className="text-xs lg:text-sm">
{t.dataPrivacyDesc}
</p>
</div>
</CardContent>

View File

@@ -1,11 +1,13 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import useStore from "@/lib/store";
import { Sparkles, FileText, ListTodo, Settings, History } from "lucide-react";
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone, Languages, Search, MessageSquare } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export type View = "enhance" | "prd" | "action" | "history" | "settings";
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "market-research" | "ai-assist" | "history" | "settings";
interface SidebarProps {
currentView: View;
@@ -13,77 +15,149 @@ interface SidebarProps {
}
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
const history = useStore((state) => state.history);
const { language, setLanguage, history } = useStore();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const t = translations[language].sidebar;
const common = translations[language].common;
const menuItems = [
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
{ id: "history" as View, label: "History", icon: History, count: history.length },
{ id: "settings" as View, label: "Settings", icon: Settings },
{ id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
{ id: "prd" as View, label: t.prdGenerator, icon: FileText },
{ id: "action" as View, label: t.actionPlan, icon: ListTodo },
{ id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
{ id: "slides" as View, label: t.slidesGen, icon: Presentation },
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone },
{ id: "market-research" as View, label: t.marketResearch, icon: Search },
{ id: "ai-assist" as View, label: t.aiAssist, icon: MessageSquare },
{ id: "history" as View, label: t.history, icon: History, count: history.length },
{ id: "settings" as View, label: t.settings, icon: Settings },
];
return (
<aside className="flex h-screen w-64 flex-col border-r bg-card">
<div className="border-b p-6">
<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">
const handleViewChange = (view: View) => {
onViewChange(view);
setIsMobileMenuOpen(false);
};
const SidebarContent = () => (
<>
<div className="border-b p-4 lg:p-6">
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
<Menu className="h-3 w-3" />
<span>{language === "en" ? "Back to rommark.dev" : language === "ru" ? "Вернуться на rommark.dev" : "חזרה ל-rommark.dev"}</span>
</a>
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
<div className="flex h-7 w-7 lg:h-8 lg:w-8 items-center justify-center rounded-lg bg-[#4285F4] text-primary-foreground text-sm lg:text-base">
PA
</div>
PromptArch
</h1>
</a>
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
<span>View on GitHub</span>
</a>
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
Forked from <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
</p>
</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) => (
<Button
key={item.id}
variant={currentView === item.id ? "default" : "ghost"}
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"
)}
onClick={() => onViewChange(item.id)}
onClick={() => handleViewChange(item.id)}
>
<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 && (
<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}
</span>
)}
</Button>
))}
<div className="mt-8 p-3 text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-4">
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
<div className="space-y-1">
<p>
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>
</p>
<p>
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
</p>
<p className="mt-2 text-[9px] opacity-80">
100% Developed using GLM 4.7 model on TRAE.AI IDE.
</p>
<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>
</p>
<div className="mt-4 p-2 lg:p-3 border-t border-border/50">
<div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
<Languages className="h-3 w-3" /> {language === "en" ? "Language" : language === "ru" ? "Язык" : "שפה"}
</div>
<div className="flex flex-wrap gap-1">
{(["en", "ru", "he"] as const).map((lang) => (
<Button
key={lang}
variant={language === lang ? "default" : "outline"}
size="sm"
className="h-7 px-2 text-[10px] uppercase font-bold"
onClick={() => setLanguage(lang)}
>
{lang}
</Button>
))}
</div>
</div>
</nav>
<div className="border-t p-4">
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground">
<div className="border-t p-3 lg:p-4 hidden lg:block">
<div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
<p className="font-medium text-foreground">Quick Tips</p>
<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> Copy enhanced prompts to your AI agent</li>
<li> PRDs generate better action plans</li>
</ul>
</div>
</div>
</>
);
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.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
PA
</div>
<span className="font-bold text-lg">PromptArch</span>
</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

@@ -0,0 +1,258 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export default function UXDesignerPrompt() {
const {
language,
currentPrompt,
enhancedPrompt,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setSelectedProvider,
setCurrentPrompt,
setEnhancedPrompt,
setProcessing,
setError,
setAvailableModels,
setSelectedModel,
} = useStore();
const t = translations[language].uxDesigner;
const common = translations[language].common;
const [copied, setCopied] = useState(false);
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
useEffect(() => {
if (typeof window !== "undefined") {
loadAvailableModels();
const saved = localStorage.getItem("promptarch-api-keys");
if (saved) {
try {
const keys = JSON.parse(saved);
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
} catch (e) {
console.error("Failed to load API keys:", e);
}
}
}
}, [selectedProvider]);
const loadAvailableModels = async () => {
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
setAvailableModels(selectedProvider, fallbackModels);
try {
const result = await modelAdapter.listModels(selectedProvider);
if (result.success && result.data) {
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
}
} catch (error) {
console.error("Failed to load models:", error);
}
};
const handleGenerate = async () => {
if (!currentPrompt.trim()) {
setError("Please enter an app description");
return;
}
const apiKey = apiKeys[selectedProvider];
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
setGeneratedPrompt(null);
console.log("[UXDesignerPrompt] Starting generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[UXDesignerPrompt] Generation result:", result);
if (result.success && result.data) {
setGeneratedPrompt(result.data);
setEnhancedPrompt(result.data);
} else {
console.error("[UXDesignerPrompt] Generation failed:", result.error);
setError(result.error || "Failed to generate UX designer prompt");
}
} catch (err) {
console.error("[UXDesignerPrompt] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => {
if (generatedPrompt) {
await navigator.clipboard.writeText(generatedPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleClear = () => {
setCurrentPrompt("");
setGeneratedPrompt(null);
setEnhancedPrompt(null);
setError(null);
};
return (
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
<Card className="h-fit">
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className={cn(
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground"
)}
>
{provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{models.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</div>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
<Textarea
placeholder={t.placeholder}
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/>
<p className="text-[10px] lg:text-xs text-muted-foreground">
{t.inputDesc}
</p>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div>
)}
</div>
)}
<div className="flex gap-2">
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? (
<>
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
{common.generating}
</>
) : (
<>
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
{language === "ru" ? "Создать UX Промпт" : language === "he" ? "חולל פרומפט UX" : "Generate UX Prompt"}
</>
)}
</Button>
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
<span className="sm:hidden">×</span>
</Button>
</div>
</CardContent>
</Card>
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
<span className="hidden sm:inline">{t.resultTitle}</span>
<span className="sm:hidden">{language === "ru" ? "UX Промпт" : language === "he" ? "פרומפט UX" : "UX Prompt"}</span>
</span>
{generatedPrompt && (
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.resultDesc}
</CardDescription>
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{generatedPrompt ? (
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{generatedPrompt}</pre>
</div>
) : (
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4 italic">
{t.emptyState}
</div>
)}
</CardContent>
</Card>
</div>
);
}

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 }

484
lib/i18n/translations.ts Normal file
View File

@@ -0,0 +1,484 @@
export type Language = "en" | "ru" | "he";
export const translations = {
en: {
sidebar: {
title: "PromptArch",
subtitle: "AI Tool Suite",
promptEnhancer: "Prompt Enhancer",
prdGenerator: "PRD Generator",
actionPlan: "Action Plan",
slidesGen: "Slides Gen",
googleAds: "Google Ads",
uxDesigner: "UX Designer",
marketResearch: "Market Research",
aiAssist: "AI Assist",
settings: "Settings",
history: "History",
},
common: {
aiProvider: "AI Provider",
model: "Model",
generate: "Generate",
generating: "Generating...",
copy: "Copy",
copied: "Copied!",
settings: "Settings",
error: "Error",
configApiKey: "Configure API key in Settings",
},
promptEnhancer: {
title: "Prompt Enhancer",
description: "Transform your simple ideas into professional, high-quality prompts",
placeholder: "Enter your prompt here...",
inputLabel: "Your Prompt",
enhancedTitle: "Enhanced Prompt",
enhancedDesc: "Your prompt has been optimized for better AI performance",
},
prdGenerator: {
title: "PRD Generator",
description: "Generate comprehensive Product Requirements Document from your idea",
placeholder: "e.g., A task management app with real-time collaboration features",
generatedTitle: "Generated PRD",
},
googleAds: {
title: "Google Ads Strategist",
description: "Generate keywords, ad copy, and campaign structure for Google Ads",
websiteUrl: "Website URL",
products: "Products / Services",
budget: "Budget (USD/mo)",
industry: "Industry",
targetAudience: "Target Audience",
specialInstructions: "Special Instructions",
specialInstructionsPlaceholder: "Any special preferences or instructions for AI to consider (e.g. 'Use a humorous tone', 'Focus on seasonal discounts')...",
generateAds: "Generate Ads",
magicWand: "Magic Wand",
researching: "Researching...",
generatedCampaign: "Generated Campaign",
strategicDirections: "Strategic Directions",
marketIntelligence: "Market Intelligence",
competitiveInsights: "Competitive Insights",
campaignDirections: "Campaign Directions",
},
settings: {
title: "System Settings",
apiKeys: "API Configuration",
qwenAuth: "Qwen Authentication",
connectQwen: "Connect Qwen Account",
connected: "Connected",
notConnected: "Not Connected",
theme: "Theme",
language: "Interface Language",
saveKeys: "Save API Keys",
keysSaved: "API keys saved successfully!",
defaultProvider: "Default Provider",
defaultProviderDesc: "Select your preferred AI provider",
dataPrivacy: "Data Privacy",
dataPrivacyDesc: "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.",
loginQwen: "Login with Qwen (OAuth)",
logoutQwen: "Logout from Qwen",
authenticated: "Authenticated via OAuth",
expires: "Expires",
enterKey: (provider: string) => `Enter your ${provider} API key`,
getApiKey: "Get API key from",
qwenDesc: "Alibaba DashScope API",
ollamaDesc: "Ollama Cloud API",
zaiDesc: "Z.AI Plan API",
},
uxDesigner: {
title: "UX Designer Prompt",
description: "Describe your app idea and get the BEST EVER prompt for UX design",
placeholder: "e.g., A fitness tracking app with workout plans, nutrition tracking, and social features...",
inputLabel: "App Description",
inputDesc: "Describe what kind of app you want, target users, key features, and any specific design preferences.",
resultTitle: "Ultimate UX Prompt",
resultDesc: "Comprehensive UX design prompt ready for designers",
emptyState: "Your comprehensive UX designer prompt will appear here",
},
history: {
title: "Session History",
description: "Previous prompts and generated results",
empty: "No history yet. Start exploring tools!",
clear: "Clear History",
},
actionPlan: {
title: "Action Plan Generator",
description: "Generate a logical, step-by-step implementation plan from your PRD",
placeholder: "Paste your PRD or project requirements here...",
generatedTitle: "Generated Action Plan",
architecture: "Technical Architecture",
infrastructure: "Infrastructure & Tools",
tasks: "Implementation Tasks",
riskAssessment: "Risk Assessment",
emptyState: "Generated action plan will appear here",
},
slidesGen: {
title: "AI Presentation Generator",
description: "Generate stunning, professional slides for any occasion in seconds",
placeholder: "Describe your presentation topic or paste an outline...",
language: "Presentation Language",
theme: "Aesthetic Theme",
audience: "Target Audience",
animations: "Animation Style",
numSlides: "Number of Slides",
generate: "Generate Presentation",
generating: "Crafting your story...",
emptyState: "Your presentation will appear here",
attachFiles: "Attach files for context",
},
marketResearch: {
title: "AI Market Research",
description: "Automated competitive intelligence and market analysis",
websiteUrl: "Company Website",
websitePlaceholder: "Primary company website (e.g., mysite.com)",
additionalUrls: "Additional Page URLs (optional)",
competitors: "Competitor Websites (3-10 recommended)",
competitorPlaceholder: "Competitor URL (e.g., competitor.com)",
productMapping: "Product/Service Comparison",
mappingPlaceholder: "Product Name/Category",
generate: "Start Research",
researching: "Performing Deep Analysis...",
emptyState: "Your comprehensive market research report will appear here",
thoughts: [
"Initializing deep scan of primary website...",
"Extracting product hierarchy and pricing structures...",
"Identifying competitor digital footprints...",
"Analyzing competitor feature sets and USPs...",
"Cross-referencing pricing data across target URLs...",
"Evaluating market positioning and landscape...",
"Synthesizing strategic advantages and identifying gaps...",
"Finalizing comprehensive intelligence report..."
]
},
aiAssist: {
title: "AI Assist",
description: "Conversational intelligence with agent switching",
placeholder: "Discuss any topic, concern or project...",
chatStart: "How can I help you today?",
switchingAgent: "Switching to specialized agent...",
routing: "Routing your request...",
preview: "Real-time Preview",
actions: "Agent Actions"
}
},
ru: {
sidebar: {
title: "PromptArch",
subtitle: "Набор ИИ-инструментов",
promptEnhancer: "Улучшение промптов",
prdGenerator: "Генератор PRD",
actionPlan: "План действий",
slidesGen: "Генератор слайдов",
googleAds: "Google Реклама",
uxDesigner: "UX Дизайнер",
marketResearch: "Анализ рынка",
aiAssist: "ИИ Ассистент",
settings: "Настройки",
history: "История",
},
common: {
aiProvider: "Провайдер ИИ",
model: "Модель",
generate: "Генерировать",
generating: "Генерация...",
copy: "Копировать",
copied: "Скопировано!",
settings: "Настройки",
error: "Ошибка",
configApiKey: "Настройте API ключ в настройках",
},
promptEnhancer: {
title: "Улучшение промптов",
description: "Превратите ваши простые идеи в профессиональные, качественные промпты",
placeholder: "Введите ваш промпт здесь...",
inputLabel: "Ваш промпт",
enhancedTitle: "Улучшенный промпт",
enhancedDesc: "Ваш промпт оптимизирован для лучшей работы ИИ",
},
prdGenerator: {
title: "Генератор PRD",
description: "Создайте подробный документ требований к продукту на основе вашей идеи",
placeholder: "Например: Приложение для управления задачами с совместной работой",
generatedTitle: "Созданный PRD",
},
googleAds: {
title: "Стратег Google Ads",
description: "Генерация ключевых слов, объявлений и структуры кампании",
websiteUrl: "URL сайта",
products: "Продукты / Услуги",
budget: "Бюджет (USD/мес)",
industry: "Отрасль",
targetAudience: "Целевая аудитория",
specialInstructions: "Особые инструкции",
specialInstructionsPlaceholder: "Любые особые предпочтения или инструкции для ИИ (например, 'Используйте юмористический тон', 'Сосредоточьтесь на сезонных скидках')...",
generateAds: "Создать рекламу",
magicWand: "Магический жезл",
researching: "Исследование...",
generatedCampaign: "Созданная кампания",
strategicDirections: "Стратегические направления",
marketIntelligence: "Анализ рынка",
competitiveInsights: "Анализ конкурентов",
campaignDirections: "Направления кампании",
},
settings: {
title: "Настройки системы",
apiKeys: "Настройка API",
qwenAuth: "Авторизация Qwen",
connectQwen: "Подключить аккаунт Qwen",
connected: "Подключено",
notConnected: "Не подключено",
theme: "Тема",
language: "Язык интерфейса",
saveKeys: "Сохранить ключи API",
keysSaved: "API ключи успешно сохранены!",
defaultProvider: "Провайдер по умолчанию",
defaultProviderDesc: "Выберите предпочитаемого провайдера ИИ",
dataPrivacy: "Конфиденциальность данных",
dataPrivacyDesc: "Все ключи API хранятся локально в вашем браузере. Ваши запросы отправляются напрямую выбранному провайдеру ИИ и не сохраняются в PromptArch.",
loginQwen: "Войти через Qwen (OAuth)",
logoutQwen: "Выйти из Qwen",
authenticated: "Авторизовано через OAuth",
expires: "Истекает",
enterKey: (provider: string) => `Введите ваш API ключ ${provider}`,
getApiKey: "Получить API ключ здесь:",
qwenDesc: "Alibaba DashScope API",
ollamaDesc: "Ollama Cloud API",
zaiDesc: "Z.AI Plan API",
},
uxDesigner: {
title: "UX Дизайнер Промпт",
description: "Опишите идею вашего приложения и получите ЛУЧШИЙ промпт для UX-дизайна",
placeholder: "Например: Приложение для отслеживания фитнеса с планами тренировок, питанием и социальными функциями...",
inputLabel: "Описание приложения",
inputDesc: "Опишите тип приложения, целевых пользователей, ключевые функции и любые предпочтения в дизайне.",
resultTitle: "Ультимативный UX промпт",
resultDesc: "Комплексный промпт для UX-дизайна, готовый для дизайнеров",
emptyState: "Ваш комплексный промпт для UX-дизайнера появится здесь",
},
history: {
title: "История сессий",
description: "Предыдущие промпты и результаты генерации",
empty: "История пока пуста. Начните использовать инструменты!",
clear: "Очистить историю",
},
actionPlan: {
title: "Генератор плана действий",
description: "Создайте логичный пошаговый план реализации на основе вашего PRD",
placeholder: "Вставьте ваш PRD или требования к проекту здесь...",
generatedTitle: "Созданный план действий",
architecture: "Техническая архитектура",
infrastructure: "Инфраструктура и инструменты",
tasks: "Задачи по реализации",
riskAssessment: "Оценка рисков",
emptyState: "Созданный план действий появится здесь",
},
slidesGen: {
title: "Генератор презентаций",
description: "Создавайте потрясающие профессиональные слайды для любого случая за считанные секунды",
placeholder: "Опишите тему презентации или вставьте план...",
language: "Язык презентации",
theme: "Эстетическая тема",
audience: "Целевая аудитория",
animations: "Стиль анимации",
numSlides: "Количество слайдов",
generate: "Создать презентацию",
generating: "Создаем вашу историю...",
emptyState: "Ваша презентация появится здесь",
attachFiles: "Прикрепить файлы для контекста",
},
marketResearch: {
title: "ИИ Анализ рынка",
description: "Автоматизированная конкурентная разведка и анализ рынка",
websiteUrl: "Сайт вашей компании",
websitePlaceholder: "Основной сайт компании (напр., mysite.ru)",
additionalUrls: "Дополнительные URL (необязательно)",
competitors: "Сайты конкурентов (рекомендуется 3-10)",
competitorPlaceholder: "URL конкурента (напр., competitor.ru)",
productMapping: "Сравнение продуктов/услуг",
mappingPlaceholder: "Название/Категория продукта",
generate: "Начать исследование",
researching: "Выполнение глубокого анализа...",
emptyState: "Ваш подробный отчет об исследовании рынка появится здесь",
thoughts: [
"Инициализация глубокого сканирования основного сайта...",
"Извлечение иерархии продуктов и структур ценообразования...",
"Идентификация цифровых следов конкурентов...",
"Анализ наборов функций и УТП конкурентов...",
"Перекрестная проверка ценовых данных по целевым URL...",
"Оценка позиционирования на рынке и ландшафта...",
"Синтез стратегических преимуществ и выявление пробелов...",
"Финализация подробного отчета о разведке..."
]
},
aiAssist: {
title: "ИИ Ассистент",
description: "Диалоговый интеллект с переключением агентов",
placeholder: "Обсудите любую тему, проблему или проект...",
chatStart: "Чем я могу помочь вам сегодня?",
switchingAgent: "Переключение на специализированного агента...",
routing: "Маршрутизация вашего запроса...",
preview: "Предпросмотр в реальном времени",
actions: "Действия агента"
}
},
he: {
sidebar: {
title: "PromptArch",
subtitle: "ערכת כלי בינה מלאכותית",
promptEnhancer: "משפר פרומפטים",
prdGenerator: "מחולל PRD",
actionPlan: "תוכנית פעולה",
slidesGen: "מחולל מצגות",
googleAds: "Google Ads",
uxDesigner: "מעצב UX",
marketResearch: "מחקר שוק",
aiAssist: "סייען AI",
settings: "הגדרות",
history: "היסטוריה",
},
common: {
aiProvider: "ספק בינה מלאכותית",
model: "מודל",
generate: "חולל",
generating: "מחולל...",
copy: "העתק",
copied: "הועתק!",
settings: "הגדרות",
error: "שגיאה",
configApiKey: "הגדר מפתח API בהגדרות",
},
promptEnhancer: {
title: "משפר פרומפטים",
description: "הפוך רעיונות פשוטים לפרומפטים מקצועיים באיכות גבוהה",
placeholder: "הזן את הפרומפט שלך כאן...",
inputLabel: "הפרומפט שלך",
enhancedTitle: "פרומפט משופר",
enhancedDesc: "הפרומפט שלך הותאם לביצועי בינה מלאכותית טובים יותר",
},
prdGenerator: {
title: "מחולל PRD",
description: "חולל מסמך דרישות מוצר מקיף מהרעיון שלך",
placeholder: "למשל: אפליקציית ניהול משימות עם תכונות שיתוף בזמן אמת",
generatedTitle: "PRD שחולל",
},
googleAds: {
title: "אסטרטג Google Ads",
description: "חולל מילות מפתח, עותקי מודעות ומבנה קמפיין",
websiteUrl: "כתובת אתר",
products: "מוצרים / שירותים",
budget: "תקציב (USD לחודש)",
industry: "תעשייה",
targetAudience: "קהל יעד",
specialInstructions: "הוראות מיוחדות",
specialInstructionsPlaceholder: "כל העדפה או הוראה מיוחדת עבור ה-AI (למשל: 'השתמש בטון הומוריסטי', 'התמקד בהנחות עונתיות')...",
generateAds: "חולל מודעות",
magicWand: "מטה קסמים",
researching: "חוקר...",
generatedCampaign: "קמפיין שחולל",
strategicDirections: "כיוונים אסטרטגיים",
marketIntelligence: "מודיעין שוק",
competitiveInsights: "תובנות תחרותיות",
campaignDirections: "כיווני קמפיין",
},
settings: {
title: "הגדרות מערכת",
apiKeys: "הגדרת API",
qwenAuth: "אימות Qwen",
connectQwen: "חבר חשבון Qwen",
connected: "מחובר",
notConnected: "לא מחובר",
theme: "עיצוב",
language: "שפת ממשק",
saveKeys: "שמור מפתחות API",
keysSaved: "מפתחות API נשמרו בהצלחה!",
defaultProvider: "ספק ברירת מחדל",
defaultProviderDesc: "בחר את ספק הבינה המלאכותית המועדף עליך",
dataPrivacy: "פרטיות נתונים",
dataPrivacyDesc: "כל מפתחות ה-API נשמרים מקומית בדפדפן שלך. הפרומפטים שלך נשלחים ישירות לספק הבינה המלאכותית הנבחר ואינם נשמרים ב-PromptArch.",
loginQwen: "התחבר עם Qwen (OAuth)",
logoutQwen: "התנתק מ-Qwen",
authenticated: "מאומת באמצעות OAuth",
expires: "פג תוקף",
enterKey: (provider: string) => `הזן את מפתח ה-API של ${provider}`,
getApiKey: "קבל מפתח API מ-",
qwenDesc: "Alibaba DashScope API",
ollamaDesc: "Ollama Cloud API",
zaiDesc: "Z.AI Plan API",
},
uxDesigner: {
title: "פרומפט מעצב UX",
description: "תאר את רעיון האפליקציה שלך וקבל את הפרומפט הטוב ביותר אי פעם לעיצוב UX",
placeholder: "למשל: אפליקציית מעקב כושר עם תוכנית אימונים, מעקב תזונה ותכונות חברתיות...",
inputLabel: "תיאור האפליקציה",
inputDesc: "תאר איזה סוג אפליקציה אתה רוצה, קהל יעד, תכונות עיקריות והעדפות עיצוב ספציפיות.",
resultTitle: "פרומפט UX אולטימטיבי",
resultDesc: "פרומפט עיצוב UX מקיף מוכן למעצבים",
emptyState: "פרומפט מעצב ה-UX המקיף שלך יופיע כאן",
},
history: {
title: "היסטוריית מפגשים",
description: "פרומפטים קודמים ותוצאות שחוללו",
empty: "אין עדיין היסטוריה. התחל לחקור את הכלים!",
clear: "נקה היסטוריה",
},
actionPlan: {
title: "מחולל תוכנית פעולה",
description: "חולל תוכנית יישום לוגית, צעד אחר צעד, ממסמך ה-PRD שלך",
placeholder: "הדבק את ה-PRD או דרישות הפרויקט שלך כאן...",
generatedTitle: "תוכנית פעולה שחוללה",
architecture: "ארכיטקטורה טכנית",
infrastructure: "תשתית וכלים",
tasks: "משימות יישום",
riskAssessment: "הערכת סיכונים",
emptyState: "תוכנית הפעולה שחוללה תופיע כאן",
},
slidesGen: {
title: "מחולל מצגות בינה מלאכותית",
description: "חולל שקופיות מרהיבות ומקצועיות לכל אירוע בשניות",
placeholder: "תאר את נושא המצגת שלך או הדבק ראשי פרקים...",
language: "שפת המצגת",
theme: "עיצוב אסתטי",
audience: "קהל יעד",
animations: "סגנון אנימציה",
numSlides: "מספר שקופיות",
generate: "חולל מצגת",
generating: "יוצר את הסיפור שלך...",
emptyState: "המצגת שלך תופיע כאן",
attachFiles: "צרף קבצים להקשר",
},
marketResearch: {
title: "מחקר שוק AI",
description: "מודיעין תחרותי וניתוח שוק אוטומטי",
websiteUrl: "אתר החברה",
websitePlaceholder: "אתר החברה הראשי (למשל: mysite.co.il)",
additionalUrls: "כתובות URL נוספות (אופציונלי)",
competitors: "אתרי מתחרים (מומלץ 3-10)",
competitorPlaceholder: "URL של מתחרה (למשל: competitor.com)",
productMapping: "השוואת מוצרים/שירותים",
mappingPlaceholder: "שם המוצר/קטגוריה",
generate: "התחל מחקר",
researching: "מבצע ניתוח מעמיק...",
emptyState: "דו\"ח מחקר השוק המקיף שלך יופיע כאן",
thoughts: [
"מאתחל סריקה עמוקה של אתר האינטרנט הראשי...",
"מחלץ היררכיית מוצרים ומבני תמחור...",
"מזהה טביעות רגל דיגיטליות של מתחרים...",
"מנתח סטים של תכונות ו-USP של מתחרים...",
"מצליב נתוני תמחור בין כתובות URL ממוקדות...",
"מעריך מיצוב שוק ונוף תחרותי...",
"מסנכרן יתרונות אסטרטגיים ומזהה פערים...",
"מגבש דו\"ח מודיעין מקיף סופי..."
]
},
aiAssist: {
title: "סייען AI",
description: "אינטליגנציה שיחתית עם החלפת סוכנים",
placeholder: "דון בכל נושא, חשש או פרויקט...",
chatStart: "במה אוכל לעזור לך היום?",
switchingAgent: "עובר לסוכן מתמחה...",
routing: "מנתב את הבקשה שלך...",
preview: "תצוגה מקדימה בזמן אמת",
actions: "פעולות סוכן"
}
}
};

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();
adapter["qwenService"]["initialize"]?.();
export default adapter;

View File

@@ -1,13 +1,10 @@
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
import QwenOAuthService from "./qwen-oauth";
import type { ModelProvider, APIResponse, ChatMessage, AIAssistMessage } from "@/types";
import OllamaCloudService from "./ollama-cloud";
import ZaiPlanService from "./zai-plan";
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
export interface ModelAdapterConfig {
qwen?: {
apiKey?: string;
endpoint?: string;
};
qwen?: QwenOAuthConfig;
ollama?: {
apiKey?: string;
endpoint?: string;
@@ -20,34 +17,35 @@ export interface ModelAdapterConfig {
}
export class ModelAdapter {
private qwenService: QwenOAuthService;
private ollamaService: OllamaCloudService;
private zaiService: ZaiPlanService;
private qwenService = qwenOAuthService;
private preferredProvider: ModelProvider;
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "qwen") {
this.qwenService = new QwenOAuthService(config.qwen);
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "ollama") {
this.ollamaService = new OllamaCloudService(config.ollama);
this.zaiService = new ZaiPlanService(config.zai);
this.preferredProvider = preferredProvider;
if (config.qwen) {
if (config.qwen.apiKey) {
this.qwenService.setApiKey(config.qwen.apiKey);
}
if (config.qwen.accessToken) {
this.qwenService.setOAuthTokens({
accessToken: config.qwen.accessToken,
refreshToken: config.qwen.refreshToken,
expiresAt: config.qwen.expiresAt,
resourceUrl: config.qwen.resourceUrl,
});
}
}
}
setPreferredProvider(provider: ModelProvider): void {
this.preferredProvider = provider;
}
updateQwenApiKey(apiKey: string): void {
this.qwenService = new QwenOAuthService({ apiKey });
}
setQwenOAuthTokens(accessToken: string, refreshToken?: string, expiresIn?: number): void {
this.qwenService.setOAuthTokens(accessToken, refreshToken, expiresIn);
}
getQwenAuthUrl(): string {
return this.qwenService.getAuthorizationUrl();
}
updateOllamaApiKey(apiKey: string): void {
this.ollamaService = new OllamaCloudService({ apiKey });
}
@@ -56,17 +54,90 @@ export class ModelAdapter {
this.zaiService = new ZaiPlanService({ apiKey });
}
updateQwenApiKey(apiKey: string): void {
this.qwenService.setApiKey(apiKey);
}
updateQwenTokens(tokens?: QwenOAuthToken): void {
this.qwenService.setOAuthTokens(tokens);
}
async startQwenOAuth(): Promise<QwenOAuthToken> {
return await this.qwenService.signIn();
}
getQwenTokenInfo(): QwenOAuthToken | null {
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[] {
const seen = new Set<ModelProvider>();
return providers.filter((provider) => {
if (seen.has(provider)) {
return false;
}
seen.add(provider);
return true;
});
}
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>(
operation: (service: any) => Promise<APIResponse<T>>,
providers: ModelProvider[]
): Promise<APIResponse<T>> {
console.log("[ModelAdapter] Attempting providers in order:", providers);
let lastError: string | null = null;
for (const provider of providers) {
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;
console.log(`[ModelAdapter] Trying provider: ${provider}`);
switch (provider) {
case "qwen":
service = this.qwenService;
console.log("[ModelAdapter] Qwen service:", {
hasApiKey: !!this.qwenService["apiKey"],
hasToken: !!this.qwenService.getTokenInfo()?.accessToken
});
break;
case "ollama":
service = this.ollamaService;
@@ -77,34 +148,184 @@ export class ModelAdapter {
}
const result = await operation(service);
console.log(`[ModelAdapter] Provider ${provider} result:`, result);
if (result.success) {
console.log(`[ModelAdapter] Success with provider: ${provider}`);
return result;
}
if (result.error) {
lastError = result.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 {
success: false,
error: finalError,
};
}
async enhancePrompt(prompt: 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.enhancePrompt(prompt, model), providers);
}
async generatePRD(idea: 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.generatePRD(idea, model), providers);
}
async generateActionPlan(prd: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
const providers: ModelProvider[] = provider ? [provider] : fallback;
return this.callWithFallback((service) => service.generateActionPlan(prd, model), providers);
}
async generateUXDesignerPrompt(appDescription: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
const providers: ModelProvider[] = provider ? [provider] : fallback;
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers);
}
async generateSlides(
topic: string,
options: {
language?: string;
theme?: string;
slideCount?: number;
audience?: string;
organization?: string;
animationStyle?: string;
audienceStyle?: string;
themeColors?: string[];
brandColors?: string[];
} = {},
provider?: ModelProvider,
model?: string
): Promise<APIResponse<string>> {
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
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: "All providers failed",
error: lastError || "No authenticated providers available for streaming",
};
}
async enhancePrompt(prompt: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
const providers: ModelProvider[] = provider ? [provider] : [this.preferredProvider, "ollama", "zai"];
return this.callWithFallback((service) => service.enhancePrompt(prompt, model), providers);
}
async generatePRD(idea: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
const providers: ModelProvider[] = provider ? [provider] : ["ollama", "zai", this.preferredProvider];
return this.callWithFallback((service) => service.generatePRD(idea, model), providers);
}
async generateActionPlan(prd: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
const providers: ModelProvider[] = provider ? [provider] : ["zai", "ollama", this.preferredProvider];
return this.callWithFallback((service) => service.generateActionPlan(prd, model), providers);
}
async chatCompletion(
messages: ChatMessage[],
@@ -137,7 +358,7 @@ export class ModelAdapter {
async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> {
const fallbackModels: Record<ModelProvider, string[]> = {
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"],
qwen: this.qwenService.getAvailableModels(),
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
};
@@ -163,16 +384,6 @@ export class ModelAdapter {
console.error("[ModelAdapter] Failed to load Z.AI models, using fallback:", error);
}
}
if (provider === "qwen" || !provider) {
try {
const qwenModels = await this.qwenService.listModels();
if (qwenModels.success && qwenModels.data && qwenModels.data.length > 0) {
models.qwen = qwenModels.data;
}
} catch (error) {
console.error("[ModelAdapter] Failed to load Qwen models, using fallback:", error);
}
}
return { success: true, data: models };
}

View File

@@ -1,15 +1,47 @@
import type { ChatMessage, APIResponse } from "@/types";
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
export interface OllamaCloudConfig {
apiKey?: string;
endpoint?: string;
}
export interface OllamaModel {
name: string;
size?: number;
digest?: string;
}
const BASE_PATH = "/tools/promptarch";
const LOCAL_MODELS_URL = `${BASE_PATH}/api/ollama/models`;
const LOCAL_CHAT_URL = `${BASE_PATH}/api/ollama/chat`;
const DEFAULT_MODELS = [
"gpt-oss:120b",
"llama3.1:latest",
"llama3.1:70b",
"llama3.1:8b",
"llama3.1:instruct",
"gemma3:12b",
"gemma3:27b",
"gemma3:4b",
"gemma3:7b",
"deepseek-r1:70b",
"deepseek-r1:32b",
"deepseek-r1:14b",
"deepseek-r1:8b",
"deepseek-r1:1.5b",
"qwen3:72b",
"qwen3:32b",
"qwen3:14b",
"qwen3:7b",
"qwen3:4b",
"mistral:7b",
"mistral:instruct",
"codellama:34b",
"codellama:13b",
"codellama:7b",
"codellama:instruct",
"phi3:14b",
"phi3:3.8b",
"phi3:mini",
"gemma2:27b",
"gemma2:9b",
"yi:34b",
"yi:9b",
];
export class OllamaCloudService {
private config: OllamaCloudConfig;
@@ -17,38 +49,50 @@ export class OllamaCloudService {
constructor(config: OllamaCloudConfig = {}) {
this.config = {
endpoint: config.endpoint || "https://ollama.com/api",
apiKey: config.apiKey || process.env.OLLAMA_API_KEY,
endpoint: config.endpoint,
};
}
private getHeaders(): Record<string, string> {
hasAuth(): boolean {
return !!this.config.apiKey;
}
private ensureApiKey(): string {
if (this.config.apiKey) {
return this.config.apiKey;
}
throw new Error("API key is required. Please configure your Ollama API key in settings.");
}
private getHeaders(additional: Record<string, string> = {}) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...additional,
"x-ollama-api-key": this.ensureApiKey(),
};
if (this.config.apiKey) {
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
if (this.config.endpoint) {
headers["x-ollama-endpoint"] = this.config.endpoint;
}
return headers;
}
private async parseJsonResponse(response: Response): Promise<any> {
const text = await response.text();
if (!text) return null;
return JSON.parse(text);
}
async chatCompletion(
messages: ChatMessage[],
model: string = "gpt-oss:120b",
stream: boolean = false
): Promise<APIResponse<string>> {
try {
if (!this.config.apiKey) {
throw new Error("API key is required. Please configure your Ollama API key in settings.");
}
console.log("[Ollama] API call:", { endpoint: this.config.endpoint, model, messages });
const response = await fetch(`${this.config.endpoint}/chat`, {
const response = await fetch(LOCAL_CHAT_URL, {
method: "POST",
headers: this.getHeaders(),
headers: this.getHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({
model,
messages,
@@ -56,24 +100,23 @@ export class OllamaCloudService {
}),
});
console.log("[Ollama] Response status:", response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
console.error("[Ollama] Error response:", errorText);
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
const errorBody = await response.text();
throw new Error(
`Chat completion failed (${response.status}): ${response.statusText} - ${errorBody}`
);
}
const data = await response.json();
console.log("[Ollama] Response data:", data);
if (data.message && data.message.content) {
const data = await this.parseJsonResponse(response);
if (data?.message?.content) {
return { success: true, data: data.message.content };
} else if (data.choices && data.choices[0]) {
return { success: true, data: data.choices[0].message.content };
} else {
return { success: false, error: "Unexpected response format" };
}
if (data?.choices?.[0]?.message?.content) {
return { success: true, data: data.choices[0].message.content };
}
return { success: false, error: "Unexpected response format" };
} catch (error) {
console.error("[Ollama] Chat completion error:", error);
return {
@@ -85,32 +128,31 @@ export class OllamaCloudService {
async listModels(): Promise<APIResponse<string[]>> {
try {
if (this.config.apiKey) {
console.log("[Ollama] Listing models from:", `${this.config.endpoint}/tags`);
const response = await fetch(`${this.config.endpoint}/tags`, {
const response = await fetch(LOCAL_MODELS_URL, {
headers: this.getHeaders(),
});
console.log("[Ollama] List models response status:", response.status, response.statusText);
if (!response.ok) {
throw new Error(`Failed to list models: ${response.statusText}`);
const errorBody = await response.text();
throw new Error(`List models failed: ${response.statusText} - ${errorBody}`);
}
const data = await response.json();
console.log("[Ollama] Models data:", data);
const models = data.models?.map((m: OllamaModel) => m.name) || [];
const data = await this.parseJsonResponse(response);
const models: string[] = Array.isArray(data?.models) ? data.models : [];
if (models.length === 0) {
this.availableModels = DEFAULT_MODELS;
return { success: true, data: DEFAULT_MODELS };
}
this.availableModels = models;
return { success: true, data: models };
} else {
console.log("[Ollama] No API key, using fallback models");
return { success: true, data: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"] };
}
} catch (error) {
console.error("[Ollama] listModels error:", error);
if (DEFAULT_MODELS.length > 0) {
this.availableModels = DEFAULT_MODELS;
return { success: true, data: DEFAULT_MODELS };
}
return {
success: false,
error: error instanceof Error ? error.message : "Failed to list models",
@@ -119,9 +161,7 @@ export class OllamaCloudService {
}
getAvailableModels(): string[] {
return this.availableModels.length > 0
? this.availableModels
: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"];
return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS;
}
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
@@ -136,7 +176,7 @@ Apply these principles:
4. Include edge cases and error handling requirements
5. Specify testing and validation criteria
Return ONLY the enhanced prompt, no explanations.`,
Return ONLY the enhanced prompt, no explanations or extra text.`,
};
const userMessage: ChatMessage = {
@@ -198,6 +238,589 @@ Include specific recommendations for:
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:
- Create EXACTLY ${slideCount} slides
- 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
}],
"campaigns": [{
"id": "campaign-1",
"name": "Campaign Name",
"type": "search",
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
"targeting": {"locations": [], "demographics": [], "devices": []},
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Maximize conversions"}]
}],
"implementation": {
"setupSteps": [],
"qualityScoreTips": [],
"trackingSetup": [],
"optimizationTips": []
},
"predictions": {
"estimatedClicks": "500-800/month",
"estimatedImpressions": "15,000-25,000/month",
"estimatedCtr": "3.2%-4.5%",
"estimatedConversions": "25-50/month"
}
}
\`\`\`
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",
"strengths": ["Strength 1", "Strength 2"],
"weaknesses": ["Weakness 1", "Weakness 2"],
"adStrategy": "Their current advertising approach"
}
],
"strategies": [
{
"id": "strategy-1",
"direction": "Strategic Direction Name",
"rationale": "Why this strategy works for this product/market",
"targetAudience": "Specific audience segment",
"competitiveAdvantage": "How this beats competitors",
"keyMessages": ["Message 1", "Message 2", "Message 3"],
"adCopyGuide": {
"headlines": ["Headline 1 (max 30 symbols)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (max 90 symbols)", "Description 2"],
"keywords": ["keyword 1", "keyword 2", "keyword 3"],
"setupGuide": "Friendly step-by-step for a beginner on where exactly to paste these in Google Ads Manager"
},
"recommendedChannels": ["Google Search", "Display", "YouTube"],
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
"expectedROI": "150-200%",
"riskLevel": "low",
"timeToResults": "2-3 months"
}
]
}
\`\`\`
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.
Your goal is to provide intelligent conversational support and switch to specialized agents.
CANVAS MODE (CRITICAL):
When the user asks to "build", "design", "create", or "write code", you MUST use the [PREVIEW] tag.
Inside [PREVIEW], output ONLY the actual functional code (HTML/Tailwind, Javascript, etc.).
Do NOT explain what the code does inside the bubble if you are generating a preview.
The user wants to see it WORKING in the Canvas immediately.
STRICT OUTPUT FORMAT:
[AGENT:id] - Optional: switch to content, seo, smm, pm, code, design, web, app.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE_OR_DATA
[/PREVIEW]
Optional conversational text (keep it brief).
Example for a mockup:
[AGENT:design]
[PREVIEW:design:html]
<div class="bg-blue-500 p-10">...</div>
[/PREVIEW]`;
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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import type { ChatMessage, APIResponse } from "@/types";
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
export interface ZaiPlanConfig {
apiKey?: string;
@@ -17,6 +17,10 @@ export class ZaiPlanService {
};
}
hasAuth(): boolean {
return !!this.config.apiKey;
}
private getHeaders(): Record<string, string> {
return {
"Content-Type": "application/json",
@@ -182,6 +186,701 @@ Include specific recommendations for:
getAvailableModels(): string[] {
return ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"];
}
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 the 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 the prompt specific, inspiring, and comprehensive. Use professional UX terminology.`,
};
const userMessage: ChatMessage = {
role: "user",
content: `Create the BEST EVER UX design prompt for this app:\n\n${appDescription}`,
};
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:
- Create EXACTLY ${slideCount} slides
- 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": "exact keyword phrase",
"type": "primary",
"searchVolume": 12000,
"competition": "medium",
"difficultyScore": 65,
"relevanceScore": 95,
"cpc": "$2.50"
}
],
"longTail": [
{
"keyword": "longer specific keyword phrase",
"type": "long-tail",
"searchVolume": 1200,
"competition": "low",
"difficultyScore": 35,
"relevanceScore": 90,
"cpc": "$1.25"
}
],
"negative": [
{
"keyword": "irrelevant term to exclude",
"type": "negative",
"competition": "low"
}
]
},
"adCopies": [
{
"id": "ad-1",
"campaignType": "search",
"headlines": [
"Headline 1 (max 30 chars)",
"Headline 2 (max 30 chars)",
"Headline 3 (max 30 chars)"
],
"descriptions": [
"Description line 1 - compelling copy under 90 chars",
"Description line 2 - call to action under 90 chars"
],
"callToAction": "Get Started Today",
"displayUrl": "example.com/offers",
"mobileOptimized": true
}
],
"campaigns": [
{
"id": "campaign-1",
"name": "Campaign Name",
"type": "search",
"budget": {
"daily": 50,
"monthly": 1500,
"currency": "USD"
},
"targeting": {
"locations": ["United States", "Canada"],
"demographics": ["25-54", "All genders"],
"devices": ["Desktop", "Mobile", "Tablet"],
"schedule": ["Mon-Fri 8am-8pm"]
},
"adGroups": [
{
"id": "adgroup-1",
"name": "Product Category Group",
"theme": "Main product focus",
"keywords": ["keyword1", "keyword2"],
"biddingStrategy": "Maximize conversions"
}
]
}
],
"implementation": {
"setupSteps": [
"Step 1: Create Google Ads account...",
"Step 2: Set up conversion tracking..."
],
"qualityScoreTips": [
"Tip 1: Match keywords to ad copy...",
"Tip 2: Optimize landing pages..."
],
"trackingSetup": [
"Install Google Tag Manager...",
"Set up conversion goals..."
],
"optimizationTips": [
"Monitor search terms weekly...",
"A/B test ad variations..."
]
},
"predictions": {
"estimatedClicks": "500-800 per month",
"estimatedImpressions": "15,000-25,000 per month",
"estimatedCtr": "3.2%-4.5%",
"estimatedConversions": "25-50 per month"
}
}
\`\`\`
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",
"strengths": ["Strength 1", "Strength 2"],
"weaknesses": ["Weakness 1", "Weakness 2"],
"adStrategy": "Their current advertising approach"
}
],
"strategies": [
{
"id": "strategy-1",
"direction": "Strategic Direction Name",
"rationale": "Why this strategy works for this product/market",
"targetAudience": "Specific audience segment",
"competitiveAdvantage": "How this beats competitors",
"keyMessages": ["Message 1", "Message 2", "Message 3"],
"adCopyGuide": {
"headlines": ["Headline 1 (max 30 symbols)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (max 90 symbols)", "Description 2"],
"keywords": ["keyword 1", "keyword 2", "keyword 3"],
"setupGuide": "Friendly step-by-step for a beginner on where exactly to paste these in Google Ads Manager"
},
"recommendedChannels": ["Google Search", "Display", "YouTube"],
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
"expectedROI": "150-200%",
"riskLevel": "low",
"timeToResults": "2-3 months"
}
]
}
\`\`\`
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".
Your goal is to provide a "Canvas" experience.
CANVAS MODE (CRITICAL):
When building or designing, you MUST use the [PREVIEW] tag.
Inside [PREVIEW], output ONLY the actual code (HTML/Tailwind etc).
The user wants to see it WORKING in the Canvas immediately.
STRICT OUTPUT FORMAT:
[AGENT:id] - Optional switch.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE
[/PREVIEW]
Optional brief text.`;
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
...options.messages.map(m => ({
role: m.role as "user" | "assistant" | "system",
content: m.content
}))
];
const endpoint = this.config.codingEndpoint; // AI Assist often involves coding
const response = await fetch(`${endpoint}/chat/completions`, {
method: "POST",
headers: this.getHeaders(),
signal: options.signal,
body: JSON.stringify({
model: model || this.getAvailableModels()[0],
messages,
stream: true,
}),
});
if (!response.ok) {
throw new Error(`Stream failed: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error("No reader");
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
if (!line.trim() || !line.startsWith("data:")) continue;
const dataStr = line.replace(/^data:\s*/, "");
if (dataStr === "[DONE]") break;
try {
const data = JSON.parse(dataStr);
const content = data.choices?.[0]?.delta?.content || data.output?.choices?.[0]?.delta?.content;
if (content) options.onChunk(content);
} catch (e) { }
}
}
return { success: true, data: undefined };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
}
}
}
export default ZaiPlanService;

View File

@@ -1,11 +1,17 @@
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 AppState {
currentPrompt: string;
enhancedPrompt: string | null;
prd: PRD | null;
actionPlan: ActionPlan | null;
slidesPresentation: SlidesPresentation | null;
googleAdsResult: GoogleAdsResult | null;
magicWandResult: MagicWandResult | null;
marketResearchResult: MarketResearchResult | null;
aiAssistHistory: AIAssistMessage[];
language: "en" | "ru" | "he";
selectedProvider: ModelProvider;
selectedModels: Record<ModelProvider, string>;
availableModels: Record<ModelProvider, string[]>;
@@ -14,7 +20,7 @@ interface AppState {
accessToken: string;
refreshToken?: string;
expiresAt?: number;
};
} | null;
isProcessing: boolean;
error: string | null;
history: {
@@ -27,11 +33,17 @@ interface AppState {
setEnhancedPrompt: (enhanced: string | null) => void;
setPRD: (prd: PRD) => void;
setActionPlan: (plan: ActionPlan) => void;
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
setMagicWandResult: (result: MagicWandResult | null) => void;
setMarketResearchResult: (result: MarketResearchResult | null) => void;
setAIAssistHistory: (history: AIAssistMessage[] | ((prev: AIAssistMessage[]) => AIAssistMessage[])) => void;
setLanguage: (lang: "en" | "ru" | "he") => void;
setSelectedProvider: (provider: ModelProvider) => void;
setSelectedModel: (provider: ModelProvider, model: string) => void;
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
setApiKey: (provider: ModelProvider, key: string) => void;
setQwenTokens: (tokens: { accessToken: string; refreshToken?: string; expiresAt?: number }) => void;
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
setProcessing: (processing: boolean) => void;
setError: (error: string | null) => void;
addToHistory: (prompt: string) => void;
@@ -44,14 +56,20 @@ const useStore = create<AppState>((set) => ({
enhancedPrompt: null,
prd: null,
actionPlan: null,
slidesPresentation: null,
googleAdsResult: null,
magicWandResult: null,
marketResearchResult: null,
aiAssistHistory: [],
language: "en",
selectedProvider: "qwen",
selectedModels: {
qwen: "qwen-coder-plus",
qwen: "coder-model",
ollama: "gpt-oss:120b",
zai: "glm-4.7",
},
availableModels: {
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"],
qwen: ["coder-model"],
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
},
@@ -68,6 +86,14 @@ const useStore = create<AppState>((set) => ({
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
setPRD: (prd) => set({ prd }),
setActionPlan: (plan) => set({ actionPlan: plan }),
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
setMagicWandResult: (result) => set({ magicWandResult: result }),
setMarketResearchResult: (result) => set({ marketResearchResult: result }),
setAIAssistHistory: (update) => set((state) => ({
aiAssistHistory: typeof update === 'function' ? update(state.aiAssistHistory) : update
})),
setLanguage: (lang) => set({ language: lang }),
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
setSelectedModel: (provider, model) =>
set((state) => ({
@@ -102,6 +128,11 @@ const useStore = create<AppState>((set) => ({
enhancedPrompt: null,
prd: null,
actionPlan: null,
slidesPresentation: null,
googleAdsResult: null,
magicWandResult: null,
marketResearchResult: null,
aiAssistHistory: [],
error: null,
}),
}));

View File

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

683
package-lock.json generated
View File

@@ -9,15 +9,17 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@radix-ui/react-tabs": "^1.1.13",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint": "^9.16.0",
"eslint-config-next": "^15.0.3",
"lucide-react": "^0.562.0",
"next": "^15.0.3",
"next": "^16.1.1",
"postcss": "^8.4.49",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -50,17 +52,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
@@ -71,16 +62,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -764,22 +745,10 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@next/env": {
"version": "15.5.9",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -792,9 +761,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
"integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
"cpu": [
"arm64"
],
@@ -808,9 +777,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
"integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
"cpu": [
"x64"
],
@@ -824,9 +793,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
"integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
"cpu": [
"arm64"
],
@@ -840,9 +809,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
"integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
"cpu": [
"arm64"
],
@@ -856,9 +825,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
"integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
"cpu": [
"x64"
],
@@ -872,9 +841,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
"integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
"cpu": [
"x64"
],
@@ -888,9 +857,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
"integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
"cpu": [
"arm64"
],
@@ -904,9 +873,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
"integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
"cpu": [
"x64"
],
@@ -963,6 +932,294 @@
"node": ">=12.4.0"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
@@ -1032,16 +1289,6 @@
"tslib": "^2.8.0"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -1188,7 +1435,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -1467,243 +1714,6 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
"integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@unrs/resolver-binding-android-arm64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
"integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@unrs/resolver-binding-darwin-arm64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
"integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@unrs/resolver-binding-darwin-x64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
"integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@unrs/resolver-binding-freebsd-x64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
"integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
"integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
"integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
"integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-arm64-musl": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
"integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
"integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
"integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
"integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
"integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-x64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
"integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-x64-musl": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
"integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-wasm32-wasi": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
"integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^0.2.11"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
"integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
"integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@unrs/resolver-binding-win32-x64-msvc": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
@@ -2324,6 +2334,18 @@
"node": ">= 6"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"
},
"funding": {
"url": "https://polar.sh/cva"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3499,20 +3521,6 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -5560,13 +5568,14 @@
"license": "MIT"
},
"node_modules/next": {
"version": "15.5.9",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
"license": "MIT",
"dependencies": {
"@next/env": "15.5.9",
"@next/env": "16.1.1",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -5575,18 +5584,18 @@
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.7",
"@next/swc-darwin-x64": "15.5.7",
"@next/swc-linux-arm64-gnu": "15.5.7",
"@next/swc-linux-arm64-musl": "15.5.7",
"@next/swc-linux-x64-gnu": "15.5.7",
"@next/swc-linux-x64-musl": "15.5.7",
"@next/swc-win32-arm64-msvc": "15.5.7",
"@next/swc-win32-x64-msvc": "15.5.7",
"sharp": "^0.34.3"
"@next/swc-darwin-arm64": "16.1.1",
"@next/swc-darwin-x64": "16.1.1",
"@next/swc-linux-arm64-gnu": "16.1.1",
"@next/swc-linux-arm64-musl": "16.1.1",
"@next/swc-linux-x64-gnu": "16.1.1",
"@next/swc-linux-x64-musl": "16.1.1",
"@next/swc-win32-arm64-msvc": "16.1.1",
"@next/swc-win32-x64-msvc": "16.1.1",
"sharp": "^0.34.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",

View File

@@ -9,15 +9,17 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-tabs": "^1.1.13",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint": "^9.16.0",
"eslint-config-next": "^15.0.3",
"lucide-react": "^0.562.0",
"next": "^15.0.3",
"next": "^16.1.1",
"postcss": "^8.4.49",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -37,7 +39,23 @@
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2"
},
"keywords": [],
"author": "",
"license": "ISC"
"keywords": [
"ai",
"prompt-engineering",
"prd-generator",
"nextjs",
"qwen",
"ollama",
"zai"
],
"author": "Roman | RyzenAdvanced <https://github.com/roman-ryzenadvanced>",
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git"
},
"bugs": {
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer/issues"
},
"homepage": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer#readme"
}

View File

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

View File

@@ -91,3 +91,217 @@ export interface ChatMessage {
role: "system" | "user" | "assistant";
content: string;
}
export interface Slide {
id: string;
title: string;
content: string;
htmlContent: string;
notes?: string;
layout: "title" | "content" | "two-column" | "image-left" | "image-right" | "quote" | "statistics" | "timeline" | "comparison";
order: number;
}
export interface SlidesPresentation {
id: string;
title: string;
subtitle?: string;
author?: string;
organization?: string;
theme: "corporate" | "modern" | "minimal" | "dark" | "vibrant" | "gradient";
language: string;
slides: Slide[];
rawContent: string;
createdAt: Date;
updatedAt: Date;
}
export interface GoogleAdsKeyword {
keyword: string;
type: "primary" | "long-tail" | "negative";
searchVolume?: number;
competition: "low" | "medium" | "high";
difficultyScore?: number;
relevanceScore?: number;
cpc?: string;
}
export interface GoogleAdCopy {
id: string;
campaignType: "search" | "display" | "shopping" | "video" | "performance-max";
headlines: string[];
descriptions: string[];
callToAction: string;
displayUrl?: string;
finalUrl?: string;
mobileOptimized: boolean;
}
export interface GoogleAdGroup {
id: string;
name: string;
theme: string;
keywords: string[];
ads: GoogleAdCopy[];
biddingStrategy?: string;
}
export interface GoogleAdsCampaign {
id: string;
name: string;
type: "search" | "display" | "shopping" | "video" | "performance-max";
budget: {
daily?: number;
monthly?: number;
currency: string;
};
targeting: {
locations?: string[];
demographics?: string[];
devices?: 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;
};
rawContent: string;
}
export interface MagicWandStrategy {
id: string;
direction: string;
rationale: string;
targetAudience: string;
competitiveAdvantage: 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;
}
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;
strengths: string[];
weaknesses: string[];
adStrategy: 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

@@ -2,5 +2,8 @@
"buildCommand": "npm run build",
"outputDirectory": ".next",
"framework": "nextjs",
"devCommand": "npm run dev"
"devCommand": "npm run dev",
"env": {
"NEXT_PUBLIC_SITE_URL": "https://traetlzlxn2t.vercel.app"
}
}