Compare commits
45 Commits
remove-qwe
...
42570a14b7
@@ -14,3 +14,7 @@ OLLAMA_ENDPOINT=https://ollama.com/api
|
|||||||
ZAI_API_KEY=
|
ZAI_API_KEY=
|
||||||
ZAI_GENERAL_ENDPOINT=https://api.z.ai/api/paas/v4
|
ZAI_GENERAL_ENDPOINT=https://api.z.ai/api/paas/v4
|
||||||
ZAI_CODING_ENDPOINT=https://api.z.ai/api/coding/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
15
LICENSE
Normal 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.
|
||||||
32
README.md
32
README.md
@@ -1,14 +1,19 @@
|
|||||||
# PromptArch: The Prompt Enhancer 🚀
|
# PromptArch: The Prompt Enhancer 🚀
|
||||||
|
|
||||||
> **Development Note**: This entire platform was developed exclusively using the [TRAE.AI IDE](https://trae.ai) powered by the elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
|
> **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
|
||||||
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
|
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> **Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
|
> **Fork Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
|
||||||
|
|
||||||
Transform vague ideas into production-ready prompts and PRDs. PromptArch is an elite AI orchestration platform designed for software architects and Vibe Coders.
|
Transform vague ideas into production-ready prompts and PRDs. PromptArch is an elite AI orchestration platform designed for software architects and Vibe Coders.
|
||||||
|
|
||||||
|
**Developed by [Roman | RyzenAdvanced](https://github.com/roman-ryzenadvanced)**
|
||||||
|
|
||||||
|
- 📦 **GitHub Repository**: [roman-ryzenadvanced/PromptArch-the-prompt-enhancer](https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer)
|
||||||
|
- 📮 **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||||
|
|
||||||
## 🌟 Visual Overview
|
## 🌟 Visual Overview
|
||||||
|
|
||||||
### 🛠 Core Capabilities
|
### 🛠 Core Capabilities
|
||||||
@@ -56,21 +61,20 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an e
|
|||||||
- **Components**: [shadcn/ui](https://ui.shadcn.com/)
|
- **Components**: [shadcn/ui](https://ui.shadcn.com/)
|
||||||
- **Icons**: [Lucide React](https://lucide.dev/)
|
- **Icons**: [Lucide React](https://lucide.dev/)
|
||||||
|
|
||||||
## 🤝 Attribution
|
## 🤝 Attribution & Credits
|
||||||
|
|
||||||
This project is a visual and architectural evolution of the [Clavix](https://github.com/ClavixDev/Clavix) framework. While Clavix focuses on agentic-first Markdown templates, PromptArch provides a centralized web interface to execute these workflows with advanced model orchestration.
|
**Author**: [Roman | RyzenAdvanced](https://github.com/roman-ryzenadvanced)
|
||||||
|
- 📦 **GitHub**: [roman-ryzenadvanced/PromptArch-the-prompt-enhancer](https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer)
|
||||||
|
- 📮 **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||||
|
|
||||||
Developed by **Roman | RyzenAdvanced**
|
**Forked from**: [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix)
|
||||||
- GitHub: [roman-ryzenadvanced](https://github.com/roman-ryzenadvanced)
|
- This project is a visual and architectural evolution of the Clavix framework
|
||||||
- Telegram: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
- Clavix focuses on agentic-first Markdown templates
|
||||||
|
- PromptArch provides a centralized web interface with advanced model orchestration
|
||||||
|
|
||||||
---
|
**Development Platform**: [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW)
|
||||||
*100% Developed using GLM 4.7 model on TRAE.AI IDE.*
|
- 100% AI-assisted development using TRAE.AI's advanced coding capabilities
|
||||||
- **Styling**: TailwindCSS
|
- Learn more about the architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW)
|
||||||
- **UI Components**: shadcn/ui + Radix UI
|
|
||||||
- **State Management**: Zustand
|
|
||||||
- **Forms**: React Hook Form + Zod
|
|
||||||
- **Icons**: Lucide React
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
108
app/api/ai-assist/route.ts
Normal file
108
app/api/ai-assist/route.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Schema validation
|
||||||
|
const schema = z.object({
|
||||||
|
request: z.string().min(1),
|
||||||
|
step: z.enum(["plan", "generate", "preview"]).default("plan"),
|
||||||
|
plan: z.any().optional(),
|
||||||
|
code: z.string().optional(),
|
||||||
|
provider: z.string().optional(),
|
||||||
|
model: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const STEPS = {
|
||||||
|
plan: `You are an expert software architect. Create a DETAILED DEVELOPMENT PLAN for the following request: "{request}"
|
||||||
|
|
||||||
|
Output ONLY a JSON object:
|
||||||
|
{
|
||||||
|
"summary": "One sentence overview",
|
||||||
|
"architecture": "High-level components + data flow",
|
||||||
|
"techStack": ["Next.js", "Tailwind", "Lucide Icons"],
|
||||||
|
"files": [
|
||||||
|
{"path": "app/page.tsx", "purpose": "Main UI"},
|
||||||
|
{"path": "components/Preview.tsx", "purpose": "Core logic"}
|
||||||
|
],
|
||||||
|
"timeline": "Estimate",
|
||||||
|
"risks": ["Potential blockers"]
|
||||||
|
}`,
|
||||||
|
|
||||||
|
generate: `You are a Senior Vibe Coder. Execute the following approved plan:
|
||||||
|
Plan: {plan}
|
||||||
|
|
||||||
|
Generate COMPLETE, PRODUCTION-READY code for all files.
|
||||||
|
Focus on the request: "{request}"
|
||||||
|
|
||||||
|
Output ONLY a JSON object:
|
||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"app/page.tsx": "// code here",
|
||||||
|
"components/UI.tsx": "// more code"
|
||||||
|
},
|
||||||
|
"explanation": "How it works"
|
||||||
|
}`,
|
||||||
|
|
||||||
|
preview: `Convert the following code into a single-file interactive HTML preview (Standalone).
|
||||||
|
Use Tailwind CDN.
|
||||||
|
|
||||||
|
Code: {code}
|
||||||
|
|
||||||
|
Output ONLY valid HTML.`
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const requestId = randomUUID();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Safe body parsing
|
||||||
|
const body = await req.json().catch(() => null);
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid JSON body", requestId, success: false },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate schema
|
||||||
|
const parseResult = schema.safeParse(body);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Invalid request body",
|
||||||
|
details: parseResult.error.flatten(),
|
||||||
|
requestId,
|
||||||
|
success: false
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { request, step, plan, code } = parseResult.data;
|
||||||
|
|
||||||
|
let prompt = STEPS[step];
|
||||||
|
prompt = prompt.replace("{request}", request);
|
||||||
|
if (plan) prompt = prompt.replace("{plan}", JSON.stringify(plan));
|
||||||
|
if (code) prompt = prompt.replace("{code}", code);
|
||||||
|
|
||||||
|
// Return the prompt for the frontend to use with the streaming adapter
|
||||||
|
return NextResponse.json({
|
||||||
|
prompt,
|
||||||
|
step,
|
||||||
|
requestId,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[ai-assist] requestId=${requestId}`, err);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: err?.message ?? "AI Assist failed",
|
||||||
|
requestId,
|
||||||
|
success: false
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/api/ollama/chat/route.ts
Normal file
69
app/api/ollama/chat/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
app/api/ollama/constants.ts
Normal file
7
app/api/ollama/constants.ts
Normal 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(/\/$/, "");
|
||||||
|
}
|
||||||
88
app/api/ollama/models/route.ts
Normal file
88
app/api/ollama/models/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
74
app/api/qwen/chat/route.ts
Normal file
74
app/api/qwen/chat/route.ts
Normal 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
22
app/api/qwen/constants.ts
Normal 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;
|
||||||
|
}
|
||||||
57
app/api/qwen/oauth/device/route.ts
Normal file
57
app/api/qwen/oauth/device/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/api/qwen/oauth/refresh/route.ts
Normal file
46
app/api/qwen/oauth/refresh/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/api/qwen/oauth/token/route.ts
Normal file
57
app/api/qwen/oauth/token/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/api/qwen/user/route.ts
Normal file
42
app/api/qwen/user/route.ts
Normal 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
37
app/api/slides/route.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
topic: z.string().min(3),
|
||||||
|
slideCount: z.number().min(3).max(15).default(8),
|
||||||
|
style: z.enum(["professional", "creative", "technical", "pitch"]).default("professional"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { topic, slideCount, style } = schema.parse(body);
|
||||||
|
|
||||||
|
const systemPrompt = `You are an elite presentation designer. Create a visually stunning presentation with ${slideCount} slides about "${topic}".
|
||||||
|
|
||||||
|
Style: ${style}
|
||||||
|
|
||||||
|
Output ONLY a sequence of slides separated by "---".
|
||||||
|
Format each slide as:
|
||||||
|
## [Slide Title]
|
||||||
|
- [Bullet Point 1]
|
||||||
|
- [Bullet Point 2]
|
||||||
|
VISUAL: [Detailed description of image/chart/icon]
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
|
||||||
|
// The frontend will handle the actual generation call to keep use of the ModelAdapter,
|
||||||
|
// this route serves as the prompt orchestrator.
|
||||||
|
return NextResponse.json({
|
||||||
|
prompt: systemPrompt,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,71 @@
|
|||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimizations */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better touch targets */
|
||||||
|
button,
|
||||||
|
a,
|
||||||
|
[role="button"] {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text selection on buttons */
|
||||||
|
button {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safe area padding for notched devices */
|
||||||
|
.safe-area-inset {
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for mobile-like experience */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: hsl(var(--border));
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
@keyframes progress-indeterminate {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-progress-indeterminate {
|
||||||
|
animation: progress-indeterminate 1.5s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Roboto } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const roboto = Roboto({
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "700"]
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "PromptArch - AI Prompt Engineering Platform",
|
title: "PromptArch - AI Prompt Engineering Platform",
|
||||||
description: "Transform vague ideas into production-ready prompts and PRDs",
|
description: "Transform vague ideas into production-ready prompts and PRDs",
|
||||||
|
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import LocaleProvider from "@/components/LocaleProvider";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
@@ -16,7 +22,11 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>{children}</body>
|
<body className={roboto.className}>
|
||||||
|
<LocaleProvider>
|
||||||
|
{children}
|
||||||
|
</LocaleProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
66
app/page.tsx
66
app/page.tsx
@@ -6,60 +6,21 @@ import type { View } from "@/components/Sidebar";
|
|||||||
import PromptEnhancer from "@/components/PromptEnhancer";
|
import PromptEnhancer from "@/components/PromptEnhancer";
|
||||||
import PRDGenerator from "@/components/PRDGenerator";
|
import PRDGenerator from "@/components/PRDGenerator";
|
||||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
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 HistoryPanel from "@/components/HistoryPanel";
|
||||||
import SettingsPanel from "@/components/SettingsPanel";
|
import SettingsPanel from "@/components/SettingsPanel";
|
||||||
import useStore from "@/lib/store";
|
|
||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [currentView, setCurrentView] = useState<View>("enhance");
|
const [currentView, setCurrentView] = useState<View>("enhance");
|
||||||
const { setQwenTokens, setApiKey } = useStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Handle OAuth callback
|
console.log("[Home] Initializing Qwen OAuth service on client...");
|
||||||
if (typeof window !== "undefined") {
|
modelAdapter["qwenService"]["initialize"]?.();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
@@ -70,6 +31,16 @@ export default function Home() {
|
|||||||
return <PRDGenerator />;
|
return <PRDGenerator />;
|
||||||
case "action":
|
case "action":
|
||||||
return <ActionPlanGenerator />;
|
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":
|
case "history":
|
||||||
return <HistoryPanel />;
|
return <HistoryPanel />;
|
||||||
case "settings":
|
case "settings":
|
||||||
@@ -82,7 +53,7 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
<div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
||||||
<Sidebar currentView={currentView} onViewChange={setCurrentView} />
|
<Sidebar currentView={currentView} onViewChange={setCurrentView} />
|
||||||
<main className="flex-1 overflow-auto p-8">
|
<main className="flex-1 overflow-auto pt-16 lg:pt-0 px-4 py-4 lg:p-8">
|
||||||
<div className="mx-auto max-w-7xl">
|
<div className="mx-auto max-w-7xl">
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
@@ -90,3 +61,4 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
808
components/AIAssist.tsx
Normal file
808
components/AIAssist.tsx
Normal 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("<") && data.includes(">");
|
||||||
|
const normalized = isEncodedHtml
|
||||||
|
? data
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/"/g, "\"")
|
||||||
|
.replace(/'/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(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/"/g, "\"")
|
||||||
|
.replace(/'/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("<") && preview.data.includes(">"));
|
||||||
|
};
|
||||||
|
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 >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function ActionPlanGenerator() {
|
export default function ActionPlanGenerator() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
actionPlan,
|
actionPlan,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
@@ -28,6 +30,9 @@ export default function ActionPlanGenerator() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].actionPlan;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
@@ -53,7 +58,7 @@ export default function ActionPlanGenerator() {
|
|||||||
const loadAvailableModels = async () => {
|
const loadAvailableModels = async () => {
|
||||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||||
setAvailableModels(selectedProvider, fallbackModels);
|
setAvailableModels(selectedProvider, fallbackModels);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.listModels(selectedProvider);
|
const result = await modelAdapter.listModels(selectedProvider);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
@@ -71,7 +76,9 @@ export default function ActionPlanGenerator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = apiKeys[selectedProvider];
|
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`);
|
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,9 +86,13 @@ export default function ActionPlanGenerator() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
|
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
|
||||||
|
|
||||||
|
console.log("[ActionPlanGenerator] Generation result:", result);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const newPlan = {
|
const newPlan = {
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
@@ -100,9 +111,11 @@ export default function ActionPlanGenerator() {
|
|||||||
};
|
};
|
||||||
setActionPlan(newPlan);
|
setActionPlan(newPlan);
|
||||||
} else {
|
} else {
|
||||||
|
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
||||||
setError(result.error || "Failed to generate action plan");
|
setError(result.error || "Failed to generate action plan");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("[ActionPlanGenerator] Generation error:", err);
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
setError(err instanceof Error ? err.message : "An error occurred");
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@@ -118,28 +131,28 @@ export default function ActionPlanGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<ListTodo className="h-5 w-5" />
|
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
Action Plan Generator
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Convert PRD into actionable implementation plan
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
key={provider}
|
key={provider}
|
||||||
variant={selectedProvider === provider ? "default" : "outline"}
|
variant={selectedProvider === provider ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setSelectedProvider(provider)}
|
onClick={() => setSelectedProvider(provider)}
|
||||||
className="capitalize"
|
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||||
>
|
>
|
||||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -147,12 +160,12 @@ export default function ActionPlanGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<option key={model} value={model}>
|
<option key={model} value={model}>
|
||||||
@@ -163,91 +176,91 @@ export default function ActionPlanGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">PRD / Requirements</label>
|
<label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Paste your PRD or project requirements here..."
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[200px] resize-y"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
|
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating Action Plan...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ListTodo className="mr-2 h-4 w-4" />
|
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate Action Plan
|
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!actionPlan && "opacity-50")}>
|
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Action Plan
|
{t.generatedTitle}
|
||||||
</span>
|
</span>
|
||||||
{actionPlan && (
|
{actionPlan && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Task breakdown, frameworks, and architecture recommendations
|
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{actionPlan ? (
|
{actionPlan ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-3 lg:space-y-4">
|
||||||
<div className="rounded-md border bg-primary/5 p-4">
|
<div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
|
||||||
<h4 className="mb-2 flex items-center gap-2 font-semibold">
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Implementation Roadmap
|
{language === "ru" ? "Дорожная карта реализации" : language === "he" ? "מפת דרכים ליישום" : "Implementation Roadmap"}
|
||||||
</h4>
|
</h4>
|
||||||
<pre className="whitespace-pre-wrap text-sm">{actionPlan.rawContent}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border bg-muted/30 p-4">
|
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||||
<h4 className="mb-2 flex items-center gap-2 font-semibold">
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Quick Notes
|
{language === "ru" ? "Быстрые заметки" : language === "he" ? "הערות מהירות" : "Quick Notes"}
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
|
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
||||||
<li>Review all task dependencies before starting</li>
|
<li>{language === "ru" ? "Проверьте все зависимости задач перед началом" : language === "he" ? "בדוק את כל התלות בין המשימות לפני שתתחיל" : "Review all task dependencies before starting"}</li>
|
||||||
<li>Set up recommended framework architecture</li>
|
<li>{language === "ru" ? "Настройте рекомендуемую архитектуру фреймворка" : language === "he" ? "הגדר את ארכיטקטורת הפרימוורק המומלצת" : "Set up recommended framework architecture"}</li>
|
||||||
<li>Follow best practices for security and performance</li>
|
<li>{language === "ru" ? "Следуйте лучшим практикам безопасности и производительности" : language === "he" ? "עקוב אחר שיטות עבודה מומלצות לאבטחה וביצועים" : "Follow best practices for security and performance"}</li>
|
||||||
<li>Use specified deployment strategy</li>
|
<li>{language === "ru" ? "Используйте указанную стратегию развертывания" : language === "he" ? "השתמש באסטרטגיית הפריסה המצוינת" : "Use specified deployment strategy"}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground">
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
Action plan will appear here
|
{t.emptyState}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
57
components/ErrorBoundary.tsx
Normal file
57
components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { AlertTriangle, RotateCcw } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetError = () => {
|
||||||
|
this.setState({ hasError: false, error: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) return this.props.fallback;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center p-8 bg-slate-50 border border-slate-200 rounded-2xl h-full text-center">
|
||||||
|
<div className="bg-rose-100 p-3 rounded-full mb-4">
|
||||||
|
<AlertTriangle className="h-6 w-6 text-rose-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-slate-900 mb-2">Something went wrong</h3>
|
||||||
|
<p className="text-sm text-slate-500 max-w-xs mb-6">
|
||||||
|
{this.state.error?.message || "An unexpected error occurred while rendering this component."}
|
||||||
|
</p>
|
||||||
|
<Button onClick={this.resetError} variant="outline">
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" /> Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
1066
components/GoogleAdsGenerator.tsx
Normal file
1066
components/GoogleAdsGenerator.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,17 +4,20 @@ import useStore from "@/lib/store";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function HistoryPanel() {
|
export default function HistoryPanel() {
|
||||||
const { history, setCurrentPrompt, clearHistory } = useStore();
|
const { language, history, setCurrentPrompt, clearHistory } = useStore();
|
||||||
|
const t = translations[language].history;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const handleRestore = (prompt: string) => {
|
const handleRestore = (prompt: string) => {
|
||||||
setCurrentPrompt(prompt);
|
setCurrentPrompt(prompt);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
if (confirm("Are you sure you want to clear all history?")) {
|
const message = language === "ru" ? "Вы уверены, что хотите очистить всю историю?" : language === "he" ? "האם אתה בטוח שברצונך למחוק את כל ההיסטוריה?" : "Are you sure you want to clear all history?";
|
||||||
|
if (confirm(message)) {
|
||||||
clearHistory();
|
clearHistory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -22,12 +25,12 @@ export default function HistoryPanel() {
|
|||||||
if (history.length === 0) {
|
if (history.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex h-[400px] items-center justify-center">
|
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6 text-center">
|
||||||
<div className="text-center">
|
<div>
|
||||||
<Clock className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
||||||
<p className="mt-4 text-muted-foreground">No history yet</p>
|
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground font-medium">{t.empty}</p>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
||||||
Start enhancing prompts to see them here
|
{language === "ru" ? "Начните использовать инструменты, чтобы увидеть историю здесь" : language === "he" ? "התחל להשתמש בכלים כדי לראות אותם כאן" : "Start using tools to see them here"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -37,35 +40,37 @@ export default function HistoryPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex-row items-center justify-between">
|
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6 text-start">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>History</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
|
||||||
<CardDescription>{history.length} items</CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{history.length} {language === "ru" ? "элем." : language === "he" ? "פריטים" : "items"}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="icon" onClick={handleClear}>
|
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9" title={t.clear}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-2 lg:space-y-3 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{history.map((item) => (
|
{history.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="rounded-md border bg-muted/30 p-4 transition-colors hover:bg-muted/50"
|
className="rounded-md border bg-muted/30 p-3 lg:p-4 transition-colors hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-1.5 lg:mb-2 flex items-center justify-between gap-2">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-[10px] lg:text-xs text-muted-foreground truncate">
|
||||||
{new Date(item.timestamp).toLocaleString()}
|
{new Date(item.timestamp).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6 flex-shrink-0"
|
||||||
onClick={() => handleRestore(item.prompt)}
|
onClick={() => handleRestore(item.prompt)}
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-3 w-3" />
|
<RotateCcw className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="line-clamp-3 text-sm">{item.prompt}</p>
|
<p className="line-clamp-3 text-xs lg:text-sm">{item.prompt}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
15
components/LocaleProvider.tsx
Normal file
15
components/LocaleProvider.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import useStore from "@/lib/store";
|
||||||
|
|
||||||
|
export default function LocaleProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const language = useStore((state) => state.language);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = language;
|
||||||
|
document.documentElement.dir = language === "he" ? "rtl" : "ltr";
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
578
components/MarketResearcher.tsx
Normal file
578
components/MarketResearcher.tsx
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import useStore from "@/lib/store";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
|
import { Search, Globe, Plus, Trash2, ShieldAlert, BarChart3, TrendingUp, Target, Rocket, Lightbulb, CheckCircle2, AlertCircle, Loader2, X, ExternalLink } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const MarketResearcher = () => {
|
||||||
|
const { language, selectedProvider, selectedModels, apiKeys, setMarketResearchResult, marketResearchResult } = useStore();
|
||||||
|
const t = translations[language].marketResearch;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
|
const [websiteUrl, setWebsiteUrl] = useState("");
|
||||||
|
const [additionalUrls, setAdditionalUrls] = useState<string[]>([""]);
|
||||||
|
const [competitorUrls, setCompetitorUrls] = useState<string[]>(["", "", ""]);
|
||||||
|
const [productMapping, setProductMapping] = useState("");
|
||||||
|
const [specialInstructions, setSpecialInstructions] = useState("");
|
||||||
|
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [thoughtIndex, setThoughtIndex] = useState(0);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
|
|
||||||
|
const handleAddUrl = () => setAdditionalUrls([...additionalUrls, ""]);
|
||||||
|
const handleRemoveUrl = (index: number) => {
|
||||||
|
const newUrls = [...additionalUrls];
|
||||||
|
newUrls.splice(index, 1);
|
||||||
|
setAdditionalUrls(newUrls);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCompetitor = () => {
|
||||||
|
if (competitorUrls.length < 10) {
|
||||||
|
setCompetitorUrls([...competitorUrls, ""]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRemoveCompetitor = (index: number) => {
|
||||||
|
const newUrls = [...competitorUrls];
|
||||||
|
newUrls.splice(index, 1);
|
||||||
|
setCompetitorUrls(newUrls);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateUrls = () => {
|
||||||
|
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
|
||||||
|
if (!websiteUrl || !urlRegex.test(websiteUrl)) return "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;
|
||||||
@@ -8,6 +8,7 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function PRDGenerator() {
|
export default function PRDGenerator() {
|
||||||
const {
|
const {
|
||||||
@@ -19,6 +20,7 @@ export default function PRDGenerator() {
|
|||||||
apiKeys,
|
apiKeys,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
error,
|
error,
|
||||||
|
language,
|
||||||
setCurrentPrompt,
|
setCurrentPrompt,
|
||||||
setSelectedProvider,
|
setSelectedProvider,
|
||||||
setPRD,
|
setPRD,
|
||||||
@@ -28,6 +30,9 @@ export default function PRDGenerator() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].prdGenerator;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -60,7 +65,7 @@ export default function PRDGenerator() {
|
|||||||
const loadAvailableModels = async () => {
|
const loadAvailableModels = async () => {
|
||||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||||
setAvailableModels(selectedProvider, fallbackModels);
|
setAvailableModels(selectedProvider, fallbackModels);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.listModels(selectedProvider);
|
const result = await modelAdapter.listModels(selectedProvider);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
@@ -78,7 +83,9 @@ export default function PRDGenerator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = apiKeys[selectedProvider];
|
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`);
|
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -86,9 +93,13 @@ export default function PRDGenerator() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
console.log("[PRDGenerator] Starting PRD generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel);
|
const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel);
|
||||||
|
|
||||||
|
console.log("[PRDGenerator] Generation result:", result);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const newPRD = {
|
const newPRD = {
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
@@ -105,9 +116,11 @@ export default function PRDGenerator() {
|
|||||||
};
|
};
|
||||||
setPRD(newPRD);
|
setPRD(newPRD);
|
||||||
} else {
|
} else {
|
||||||
|
console.error("[PRDGenerator] Generation failed:", result.error);
|
||||||
setError(result.error || "Failed to generate PRD");
|
setError(result.error || "Failed to generate PRD");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("[PRDGenerator] Generation error:", err);
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
setError(err instanceof Error ? err.message : "An error occurred");
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@@ -123,37 +136,37 @@ export default function PRDGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ id: "overview", title: "Overview & Objectives" },
|
{ id: "overview", title: language === "ru" ? "Обзор продукта" : language === "he" ? "סקירת מוצר" : "Product Overview" },
|
||||||
{ id: "personas", title: "User Personas & Use Cases" },
|
{ id: "personas", title: language === "ru" ? "Персоны пользователей" : language === "he" ? "פרסונות משתמשים" : "User Personas & Use Cases" },
|
||||||
{ id: "functional", title: "Functional Requirements" },
|
{ id: "functional", title: language === "ru" ? "Функциональные требования" : language === "he" ? "דרישות פונקציונליות" : "Functional Requirements" },
|
||||||
{ id: "nonfunctional", title: "Non-functional Requirements" },
|
{ id: "nonfunctional", title: language === "ru" ? "Нефункциональные требования" : language === "he" ? "דרישות לא פונקציונליות" : "Non-functional Requirements" },
|
||||||
{ id: "architecture", title: "Technical Architecture" },
|
{ id: "architecture", title: language === "ru" ? "Техническая архитектура" : language === "he" ? "ארכיטקטורה טכנית" : "Technical Architecture" },
|
||||||
{ id: "metrics", title: "Success Metrics" },
|
{ id: "metrics", title: language === "ru" ? "Успешность метрик" : language === "he" ? "מדדי הצלחה" : "Success Metrics" },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<FileText className="h-5 w-5" />
|
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
PRD Generator
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Generate comprehensive Product Requirements Document from your idea
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
key={provider}
|
key={provider}
|
||||||
variant={selectedProvider === provider ? "default" : "outline"}
|
variant={selectedProvider === provider ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setSelectedProvider(provider)}
|
onClick={() => setSelectedProvider(provider)}
|
||||||
className="capitalize"
|
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||||
>
|
>
|
||||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -161,12 +174,12 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<option key={model} value={model}>
|
<option key={model} value={model}>
|
||||||
@@ -177,37 +190,36 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Your Idea</label>
|
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., A task management app with real-time collaboration features"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[200px] resize-y"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
|
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating PRD...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FileText className="mr-2 h-4 w-4" />
|
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate PRD
|
{common.generate}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -215,53 +227,53 @@ export default function PRDGenerator() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!prd && "opacity-50")}>
|
<Card className={cn(!prd && "opacity-50")}>
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Generated PRD
|
{t.generatedTitle}
|
||||||
</span>
|
</span>
|
||||||
{prd && (
|
{prd && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Structured requirements document ready for development
|
{language === "ru" ? "Структурированный документ требований готов к разработке" : language === "he" ? "מסמך דרישות מובנה מוכן לפיתוח" : "Structured requirements document ready for development"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{prd ? (
|
{prd ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-2 lg:space-y-3">
|
||||||
{sections.map((section) => (
|
{sections.map((section) => (
|
||||||
<div key={section.id} className="rounded-md border bg-muted/30">
|
<div key={section.id} className="rounded-md border bg-muted/30">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSection(section.id)}
|
onClick={() => toggleSection(section.id)}
|
||||||
className="flex w-full items-center justify-between px-4 py-3 text-left font-medium transition-colors hover:bg-muted/50"
|
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
||||||
>
|
>
|
||||||
<span>{section.title}</span>
|
<span>{section.title}</span>
|
||||||
{expandedSections.includes(section.id) ? (
|
{expandedSections.includes(section.id) ? (
|
||||||
<ChevronUp className="h-4 w-4" />
|
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{expandedSections.includes(section.id) && (
|
{expandedSections.includes(section.id) && (
|
||||||
<div className="border-t bg-background px-4 py-3">
|
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3">
|
||||||
<pre className="whitespace-pre-wrap text-sm">{prd.overview}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{prd.overview}</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground">
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||||
PRD will appear here
|
{language === "ru" ? "Здесь появится созданный PRD" : language === "he" ? "PRD שחולל יופיע כאן" : "Generated PRD will appear here"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function PromptEnhancer() {
|
export default function PromptEnhancer() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
enhancedPrompt,
|
enhancedPrompt,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
@@ -28,6 +30,9 @@ export default function PromptEnhancer() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].promptEnhancer;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
@@ -53,7 +58,7 @@ export default function PromptEnhancer() {
|
|||||||
const loadAvailableModels = async () => {
|
const loadAvailableModels = async () => {
|
||||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||||
setAvailableModels(selectedProvider, fallbackModels);
|
setAvailableModels(selectedProvider, fallbackModels);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.listModels(selectedProvider);
|
const result = await modelAdapter.listModels(selectedProvider);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
@@ -71,7 +76,9 @@ export default function PromptEnhancer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = apiKeys[selectedProvider];
|
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`);
|
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,15 +86,21 @@ export default function PromptEnhancer() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
|
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
|
||||||
|
|
||||||
|
console.log("[PromptEnhancer] Enhancement result:", result);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setEnhancedPrompt(result.data);
|
setEnhancedPrompt(result.data);
|
||||||
} else {
|
} else {
|
||||||
|
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
||||||
setError(result.error || "Failed to enhance prompt");
|
setError(result.error || "Failed to enhance prompt");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("[PromptEnhancer] Enhancement error:", err);
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
setError(err instanceof Error ? err.message : "An error occurred");
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@@ -109,21 +122,21 @@ export default function PromptEnhancer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Sparkles className="h-5 w-5" />
|
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
Input Prompt
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Enter your prompt and we'll enhance it for AI coding agents
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
key={provider}
|
key={provider}
|
||||||
@@ -131,7 +144,7 @@ export default function PromptEnhancer() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setSelectedProvider(provider)}
|
onClick={() => setSelectedProvider(provider)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"capitalize",
|
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
||||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -141,12 +154,12 @@ export default function PromptEnhancer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<option key={model} value={model}>
|
<option key={model} value={model}>
|
||||||
@@ -156,79 +169,79 @@ export default function PromptEnhancer() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-sm font-medium">Your Prompt</label>
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., Create a user authentication system with JWT tokens"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[200px] resize-y"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1">
|
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Enhancing...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Sparkles className="mr-2 h-4 w-4" />
|
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Enhance Prompt
|
{t.title}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing}>
|
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Clear
|
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!enhancedPrompt && "opacity-50")}>
|
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Enhanced Prompt
|
{t.enhancedTitle}
|
||||||
</span>
|
</span>
|
||||||
{enhancedPrompt && (
|
{enhancedPrompt && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Professional prompt ready for coding agents
|
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{enhancedPrompt ? (
|
{enhancedPrompt ? (
|
||||||
<div className="rounded-md border bg-muted/50 p-4">
|
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
<pre className="whitespace-pre-wrap text-sm">{enhancedPrompt}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[200px] items-center justify-center text-center text-sm text-muted-foreground">
|
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
Enhanced prompt will appear here
|
{language === "ru" ? "Улучшенный промпт появится здесь" : language === "he" ? "פרומפט משופר יופיע כאן" : "Enhanced prompt will appear here"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -3,21 +3,23 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import useStore from "@/lib/store";
|
import useStore from "@/lib/store";
|
||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function SettingsPanel() {
|
export default function SettingsPanel() {
|
||||||
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
const { language, apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
||||||
|
const t = translations[language].settings;
|
||||||
|
const common = translations[language].common;
|
||||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||||
|
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
|
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
|
||||||
alert("API keys saved successfully!");
|
alert(t.keysSaved);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,12 +45,16 @@ export default function SettingsPanel() {
|
|||||||
console.error("Failed to load API keys:", e);
|
console.error("Failed to load API keys:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const storedTokens = modelAdapter.getQwenTokenInfo();
|
||||||
|
if (storedTokens) {
|
||||||
|
setQwenTokens(storedTokens);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApiKeyChange = (provider: string, value: string) => {
|
const handleApiKeyChange = (provider: string, value: string) => {
|
||||||
setApiKey(provider as "qwen" | "ollama" | "zai", value);
|
setApiKey(provider as "qwen" | "ollama" | "zai", value);
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case "qwen":
|
case "qwen":
|
||||||
modelAdapter.updateQwenApiKey(value);
|
modelAdapter.updateQwenApiKey(value);
|
||||||
@@ -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(() => {
|
useEffect(() => {
|
||||||
handleLoad();
|
handleLoad();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl space-y-6">
|
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Key className="h-5 w-5" />
|
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
API Configuration
|
{t.apiKeys}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Configure API keys for different AI providers
|
{language === "ru" ? "Настройте ключи API для различных провайдеров ИИ" : language === "he" ? "הגדר מפתחות API עבור ספקי בינה מלאכותית שונים" : "Configure API keys for different AI providers"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Qwen Code API Key
|
Qwen Code API Key
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.qwen ? "text" : "password"}
|
type={showApiKey.qwen ? "text" : "password"}
|
||||||
placeholder="Enter your Qwen API key"
|
placeholder={t.enterKey("Qwen")}
|
||||||
value={apiKeys.qwen || ""}
|
value={apiKeys.qwen || ""}
|
||||||
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
||||||
className="font-mono text-sm"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute right-0 top-0 h-full"
|
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||||
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
|
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
|
||||||
>
|
>
|
||||||
{showApiKey.qwen ? (
|
{showApiKey.qwen ? (
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
) : (
|
) : (
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
|
||||||
<p className="text-xs text-muted-foreground flex-1">
|
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://help.aliyun.com/zh/dashscope/"
|
href="https://help.aliyun.com/zh/dashscope/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -121,56 +150,53 @@ export default function SettingsPanel() {
|
|||||||
<Button
|
<Button
|
||||||
variant={qwenTokens ? "secondary" : "outline"}
|
variant={qwenTokens ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8"
|
className="h-7 lg:h-8 text-[10px] lg:text-xs w-full sm:w-auto"
|
||||||
onClick={() => {
|
onClick={handleQwenAuth}
|
||||||
if (qwenTokens) {
|
disabled={isAuthLoading}
|
||||||
setQwenTokens(undefined as any);
|
|
||||||
localStorage.removeItem("promptarch-qwen-tokens");
|
|
||||||
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
|
|
||||||
} else {
|
|
||||||
window.location.href = modelAdapter.getQwenAuthUrl();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{qwenTokens ? "Logout from Qwen" : "Login with Qwen (OAuth)"}
|
{isAuthLoading
|
||||||
|
? (language === "ru" ? "Вход..." : language === "he" ? "מתחבר..." : "Signing in...")
|
||||||
|
: qwenTokens
|
||||||
|
? t.logoutQwen
|
||||||
|
: t.loginQwen}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{qwenTokens && (
|
{qwenTokens && (
|
||||||
<p className="text-[10px] text-green-600 dark:text-green-400 font-medium">
|
<p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
|
||||||
✓ Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
✓ {t.authenticated} ({t.expires}: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Ollama Cloud API Key
|
Ollama Cloud API Key
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.ollama ? "text" : "password"}
|
type={showApiKey.ollama ? "text" : "password"}
|
||||||
placeholder="Enter your Ollama API key"
|
placeholder={t.enterKey("Ollama")}
|
||||||
value={apiKeys.ollama || ""}
|
value={apiKeys.ollama || ""}
|
||||||
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
||||||
className="font-mono text-sm"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute right-0 top-0 h-full"
|
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||||
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
|
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
|
||||||
>
|
>
|
||||||
{showApiKey.ollama ? (
|
{showApiKey.ollama ? (
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
) : (
|
) : (
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://ollama.com/cloud"
|
href="https://ollama.com/cloud"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -182,35 +208,35 @@ export default function SettingsPanel() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Z.AI Plan API Key
|
Z.AI Plan API Key
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.zai ? "text" : "password"}
|
type={showApiKey.zai ? "text" : "password"}
|
||||||
placeholder="Enter your Z.AI API key"
|
placeholder={t.enterKey("Z.AI")}
|
||||||
value={apiKeys.zai || ""}
|
value={apiKeys.zai || ""}
|
||||||
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
||||||
className="font-mono text-sm"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute right-0 top-0 h-full"
|
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||||
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
|
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
|
||||||
>
|
>
|
||||||
{showApiKey.zai ? (
|
{showApiKey.zai ? (
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
) : (
|
) : (
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.z.ai"
|
href="https://docs.z.ai"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -222,45 +248,44 @@ export default function SettingsPanel() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleSave} className="w-full">
|
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
<Save className="mr-2 h-4 w-4" />
|
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Save API Keys
|
{t.saveKeys}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle>Default Provider</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.defaultProvider}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Select your preferred AI provider
|
{t.defaultProviderDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-2 lg:gap-3">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<button
|
<button
|
||||||
key={provider}
|
key={provider}
|
||||||
onClick={() => setSelectedProvider(provider)}
|
onClick={() => setSelectedProvider(provider)}
|
||||||
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 ${
|
className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg:p-4 text-left transition-colors hover:bg-muted/50 ${selectedProvider === provider
|
||||||
selectedProvider === provider
|
? "border-primary bg-primary/5"
|
||||||
? "border-primary bg-primary/5"
|
: "border-border"
|
||||||
: "border-border"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10">
|
<div className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
|
||||||
<Server className="h-5 w-5 text-primary" />
|
<Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-medium capitalize">{provider}</h3>
|
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||||
{provider === "qwen" && "Alibaba DashScope API"}
|
{provider === "qwen" && t.qwenDesc}
|
||||||
{provider === "ollama" && "Ollama Cloud API"}
|
{provider === "ollama" && t.ollamaDesc}
|
||||||
{provider === "zai" && "Z.AI Plan API"}
|
{provider === "zai" && t.zaiDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedProvider === provider && (
|
{selectedProvider === provider && (
|
||||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
<div className="h-2 w-2 rounded-full bg-primary flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -269,16 +294,16 @@ export default function SettingsPanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle>Data Privacy</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Your data handling preferences
|
{language === "ru" ? "Ваши настройки обработки данных" : language === "he" ? "העדפות הטיפול בנתונים שלך" : "Your data handling preferences"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="rounded-md border bg-muted/30 p-4">
|
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||||
<p className="text-sm">
|
<p className="text-xs lg:text-sm">
|
||||||
All API keys are stored locally in your browser. Your prompts are sent directly to the selected AI provider and are not stored by PromptArch.
|
{t.dataPrivacyDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useStore from "@/lib/store";
|
import useStore from "@/lib/store";
|
||||||
import { Sparkles, FileText, ListTodo, 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 { 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 {
|
interface SidebarProps {
|
||||||
currentView: View;
|
currentView: View;
|
||||||
@@ -13,77 +15,149 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||||
const history = useStore((state) => state.history);
|
const { language, setLanguage, history } = useStore();
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const t = translations[language].sidebar;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
|
{ id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
|
||||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
{ id: "prd" as View, label: t.prdGenerator, icon: FileText },
|
||||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
{ id: "action" as View, label: t.actionPlan, icon: ListTodo },
|
||||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
{ id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
|
||||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
{ 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 (
|
const handleViewChange = (view: View) => {
|
||||||
<aside className="flex h-screen w-64 flex-col border-r bg-card">
|
onViewChange(view);
|
||||||
<div className="border-b p-6">
|
setIsMobileMenuOpen(false);
|
||||||
<h1 className="flex items-center gap-2 text-xl font-bold">
|
};
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
||||||
PA
|
const SidebarContent = () => (
|
||||||
</div>
|
<>
|
||||||
PromptArch
|
<div className="border-b p-4 lg:p-6">
|
||||||
</h1>
|
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||||
|
<Menu className="h-3 w-3" />
|
||||||
|
<span>{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>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 space-y-1 p-4">
|
<nav className="flex-1 space-y-1 p-3 lg:p-4 overflow-y-auto">
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<Button
|
<Button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
variant={currentView === item.id ? "default" : "ghost"}
|
variant={currentView === item.id ? "default" : "ghost"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-start gap-2",
|
"w-full justify-start gap-2 h-9 lg:h-10 text-sm",
|
||||||
currentView === item.id && "bg-primary text-primary-foreground"
|
currentView === item.id && "bg-primary text-primary-foreground"
|
||||||
)}
|
)}
|
||||||
onClick={() => onViewChange(item.id)}
|
onClick={() => handleViewChange(item.id)}
|
||||||
>
|
>
|
||||||
<item.icon className="h-4 w-4" />
|
<item.icon className="h-4 w-4" />
|
||||||
<span className="flex-1 text-left">{item.label}</span>
|
<span className="flex-1 text-left truncate">{item.label}</span>
|
||||||
{item.count !== undefined && item.count > 0 && (
|
{item.count !== undefined && item.count > 0 && (
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium">
|
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium text-primary">
|
||||||
{item.count}
|
{item.count}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mt-8 p-3 text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-4">
|
<div className="mt-4 p-2 lg:p-3 border-t border-border/50">
|
||||||
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
|
<div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
|
||||||
<div className="space-y-1">
|
<Languages className="h-3 w-3" /> {language === "en" ? "Language" : language === "ru" ? "Язык" : "שפה"}
|
||||||
<p>
|
</div>
|
||||||
GitHub: <a href="https://github.com/roman-ryzenadvanced/Custom-Engineered-Agents-and-Tools-for-Vibe-Coders" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a>
|
<div className="flex flex-wrap gap-1">
|
||||||
</p>
|
{(["en", "ru", "he"] as const).map((lang) => (
|
||||||
<p>
|
<Button
|
||||||
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
|
key={lang}
|
||||||
</p>
|
variant={language === lang ? "default" : "outline"}
|
||||||
<p className="mt-2 text-[9px] opacity-80">
|
size="sm"
|
||||||
100% Developed using GLM 4.7 model on TRAE.AI IDE.
|
className="h-7 px-2 text-[10px] uppercase font-bold"
|
||||||
</p>
|
onClick={() => setLanguage(lang)}
|
||||||
<p className="text-[9px] opacity-80">
|
>
|
||||||
Model Info: <a href="https://z.ai/subscribe?ic=R0K78RJKNW" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Learn here</a>
|
{lang}
|
||||||
</p>
|
</Button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="border-t p-4">
|
<div className="border-t p-3 lg:p-4 hidden lg:block">
|
||||||
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground">
|
<div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
|
||||||
<p className="font-medium text-foreground">Quick Tips</p>
|
<p className="font-medium text-foreground">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>• Use different providers for best results</li>
|
||||||
<li>• Copy enhanced prompts to your AI agent</li>
|
<li>• Copy enhanced prompts to your AI agent</li>
|
||||||
<li>• PRDs generate better action plans</li>
|
<li>• PRDs generate better action plans</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile Header */}
|
||||||
|
<div className="lg:hidden fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b bg-card px-4 py-3">
|
||||||
|
<a href="https://github.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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1347
components/SlidesGenerator.tsx
Normal file
1347
components/SlidesGenerator.tsx
Normal file
File diff suppressed because it is too large
Load Diff
258
components/UXDesignerPrompt.tsx
Normal file
258
components/UXDesignerPrompt.tsx
Normal 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
36
components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> { }
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
55
components/ui/tabs.tsx
Normal file
55
components/ui/tabs.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
484
lib/i18n/translations.ts
Normal file
484
lib/i18n/translations.ts
Normal 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
73
lib/safeJsonFetch.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
export class NonJsonResponseError extends Error {
|
||||||
|
status: number;
|
||||||
|
contentType: string | null;
|
||||||
|
bodyPreview: string;
|
||||||
|
|
||||||
|
constructor(args: { status: number; contentType: string | null; bodyPreview: string }) {
|
||||||
|
super(`Expected JSON but received ${args.contentType ?? "unknown content-type"} (HTTP ${args.status})`);
|
||||||
|
this.name = "NonJsonResponseError";
|
||||||
|
this.status = args.status;
|
||||||
|
this.contentType = args.contentType;
|
||||||
|
this.bodyPreview = args.bodyPreview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SafeJsonFetchResult<T> =
|
||||||
|
| { ok: true; data: T }
|
||||||
|
| { ok: false; error: { message: string; status?: number; bodyPreview?: string } };
|
||||||
|
|
||||||
|
export async function safeJsonFetch<T>(
|
||||||
|
url: string,
|
||||||
|
init?: RequestInit
|
||||||
|
): Promise<SafeJsonFetchResult<T>> {
|
||||||
|
const res = await fetch(url, init);
|
||||||
|
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
// HTTP error — return readable details (don't JSON.parse blindly)
|
||||||
|
if (!res.ok) {
|
||||||
|
// Try JSON first if it looks like JSON
|
||||||
|
if (contentType?.includes("application/json")) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
return { ok: false, error: { message: parsed?.error ?? "Request failed", status: res.status } };
|
||||||
|
} catch {
|
||||||
|
// fall through to generic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: `Request failed (HTTP ${res.status})`,
|
||||||
|
status: res.status,
|
||||||
|
bodyPreview: text.slice(0, 300),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success but not JSON => this is exactly the "Unexpected token <" case
|
||||||
|
if (!contentType?.includes("application/json")) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: `Server returned non-JSON (content-type: ${contentType ?? "unknown"})`,
|
||||||
|
status: res.status,
|
||||||
|
bodyPreview: text.slice(0, 300),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return { ok: true, data: JSON.parse(text) as T };
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: "Server returned invalid JSON",
|
||||||
|
status: res.status,
|
||||||
|
bodyPreview: text.slice(0, 300),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,6 @@ import ModelAdapter from "./model-adapter";
|
|||||||
|
|
||||||
const adapter = new ModelAdapter();
|
const adapter = new ModelAdapter();
|
||||||
|
|
||||||
|
adapter["qwenService"]["initialize"]?.();
|
||||||
|
|
||||||
export default adapter;
|
export default adapter;
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
|
import type { ModelProvider, APIResponse, ChatMessage, AIAssistMessage } from "@/types";
|
||||||
import QwenOAuthService from "./qwen-oauth";
|
|
||||||
import OllamaCloudService from "./ollama-cloud";
|
import OllamaCloudService from "./ollama-cloud";
|
||||||
import ZaiPlanService from "./zai-plan";
|
import ZaiPlanService from "./zai-plan";
|
||||||
|
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
|
||||||
|
|
||||||
export interface ModelAdapterConfig {
|
export interface ModelAdapterConfig {
|
||||||
qwen?: {
|
qwen?: QwenOAuthConfig;
|
||||||
apiKey?: string;
|
|
||||||
endpoint?: string;
|
|
||||||
};
|
|
||||||
ollama?: {
|
ollama?: {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
@@ -20,34 +17,35 @@ export interface ModelAdapterConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ModelAdapter {
|
export class ModelAdapter {
|
||||||
private qwenService: QwenOAuthService;
|
|
||||||
private ollamaService: OllamaCloudService;
|
private ollamaService: OllamaCloudService;
|
||||||
private zaiService: ZaiPlanService;
|
private zaiService: ZaiPlanService;
|
||||||
|
private qwenService = qwenOAuthService;
|
||||||
private preferredProvider: ModelProvider;
|
private preferredProvider: ModelProvider;
|
||||||
|
|
||||||
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "qwen") {
|
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "ollama") {
|
||||||
this.qwenService = new QwenOAuthService(config.qwen);
|
|
||||||
this.ollamaService = new OllamaCloudService(config.ollama);
|
this.ollamaService = new OllamaCloudService(config.ollama);
|
||||||
this.zaiService = new ZaiPlanService(config.zai);
|
this.zaiService = new ZaiPlanService(config.zai);
|
||||||
this.preferredProvider = preferredProvider;
|
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 {
|
setPreferredProvider(provider: ModelProvider): void {
|
||||||
this.preferredProvider = provider;
|
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 {
|
updateOllamaApiKey(apiKey: string): void {
|
||||||
this.ollamaService = new OllamaCloudService({ apiKey });
|
this.ollamaService = new OllamaCloudService({ apiKey });
|
||||||
}
|
}
|
||||||
@@ -56,17 +54,90 @@ export class ModelAdapter {
|
|||||||
this.zaiService = new ZaiPlanService({ apiKey });
|
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>(
|
private async callWithFallback<T>(
|
||||||
operation: (service: any) => Promise<APIResponse<T>>,
|
operation: (service: any) => Promise<APIResponse<T>>,
|
||||||
providers: ModelProvider[]
|
providers: ModelProvider[]
|
||||||
): Promise<APIResponse<T>> {
|
): Promise<APIResponse<T>> {
|
||||||
|
console.log("[ModelAdapter] Attempting providers in order:", providers);
|
||||||
|
let lastError: string | null = null;
|
||||||
|
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
try {
|
try {
|
||||||
|
console.log(`[ModelAdapter] Checking authentication for ${provider}...`);
|
||||||
|
|
||||||
|
if (!this.isProviderAuthenticated(provider)) {
|
||||||
|
console.log(`[ModelAdapter] Provider ${provider} is not authenticated, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let service: any;
|
let service: any;
|
||||||
|
|
||||||
|
console.log(`[ModelAdapter] Trying provider: ${provider}`);
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case "qwen":
|
case "qwen":
|
||||||
service = this.qwenService;
|
service = this.qwenService;
|
||||||
|
console.log("[ModelAdapter] Qwen service:", {
|
||||||
|
hasApiKey: !!this.qwenService["apiKey"],
|
||||||
|
hasToken: !!this.qwenService.getTokenInfo()?.accessToken
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "ollama":
|
case "ollama":
|
||||||
service = this.ollamaService;
|
service = this.ollamaService;
|
||||||
@@ -77,34 +148,184 @@ export class ModelAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await operation(service);
|
const result = await operation(service);
|
||||||
|
console.log(`[ModelAdapter] Provider ${provider} result:`, result);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
console.log(`[ModelAdapter] Success with provider: ${provider}`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
lastError = result.error;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error with ${provider}:`, error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[ModelAdapter] Error with ${provider}:`, errorMessage);
|
||||||
|
lastError = errorMessage || lastError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalError = lastError
|
||||||
|
? `All providers failed: ${lastError}`
|
||||||
|
: "All providers failed. Please configure API key in Settings";
|
||||||
|
console.error(`[ModelAdapter] ${finalError}`);
|
||||||
|
return {
|
||||||
|
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 {
|
return {
|
||||||
success: false,
|
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(
|
async chatCompletion(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
@@ -137,12 +358,12 @@ export class ModelAdapter {
|
|||||||
|
|
||||||
async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> {
|
async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> {
|
||||||
const fallbackModels: 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"],
|
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"],
|
zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||||
};
|
};
|
||||||
const models: Record<ModelProvider, string[]> = { ...fallbackModels };
|
const models: Record<ModelProvider, string[]> = { ...fallbackModels };
|
||||||
|
|
||||||
if (provider === "ollama" || !provider) {
|
if (provider === "ollama" || !provider) {
|
||||||
try {
|
try {
|
||||||
const ollamaModels = await this.ollamaService.listModels();
|
const ollamaModels = await this.ollamaService.listModels();
|
||||||
@@ -163,16 +384,6 @@ export class ModelAdapter {
|
|||||||
console.error("[ModelAdapter] Failed to load Z.AI models, using fallback:", error);
|
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 };
|
return { success: true, data: models };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,47 @@
|
|||||||
import type { ChatMessage, APIResponse } from "@/types";
|
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||||
|
|
||||||
export interface OllamaCloudConfig {
|
export interface OllamaCloudConfig {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OllamaModel {
|
const BASE_PATH = "/tools/promptarch";
|
||||||
name: string;
|
const LOCAL_MODELS_URL = `${BASE_PATH}/api/ollama/models`;
|
||||||
size?: number;
|
const LOCAL_CHAT_URL = `${BASE_PATH}/api/ollama/chat`;
|
||||||
digest?: string;
|
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 {
|
export class OllamaCloudService {
|
||||||
private config: OllamaCloudConfig;
|
private config: OllamaCloudConfig;
|
||||||
@@ -17,38 +49,50 @@ export class OllamaCloudService {
|
|||||||
|
|
||||||
constructor(config: OllamaCloudConfig = {}) {
|
constructor(config: OllamaCloudConfig = {}) {
|
||||||
this.config = {
|
this.config = {
|
||||||
endpoint: config.endpoint || "https://ollama.com/api",
|
|
||||||
apiKey: config.apiKey || process.env.OLLAMA_API_KEY,
|
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> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
...additional,
|
||||||
|
"x-ollama-api-key": this.ensureApiKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.config.apiKey) {
|
if (this.config.endpoint) {
|
||||||
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
headers["x-ollama-endpoint"] = this.config.endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async parseJsonResponse(response: Response): Promise<any> {
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text) return null;
|
||||||
|
return JSON.parse(text);
|
||||||
|
}
|
||||||
|
|
||||||
async chatCompletion(
|
async chatCompletion(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
model: string = "gpt-oss:120b",
|
model: string = "gpt-oss:120b",
|
||||||
stream: boolean = false
|
stream: boolean = false
|
||||||
): Promise<APIResponse<string>> {
|
): Promise<APIResponse<string>> {
|
||||||
try {
|
try {
|
||||||
if (!this.config.apiKey) {
|
const response = await fetch(LOCAL_CHAT_URL, {
|
||||||
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`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders({ "Content-Type": "application/json" }),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
messages,
|
messages,
|
||||||
@@ -56,24 +100,23 @@ export class OllamaCloudService {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[Ollama] Response status:", response.status, response.statusText);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorBody = await response.text();
|
||||||
console.error("[Ollama] Error response:", errorText);
|
throw new Error(
|
||||||
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
|
`Chat completion failed (${response.status}): ${response.statusText} - ${errorBody}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await this.parseJsonResponse(response);
|
||||||
console.log("[Ollama] Response data:", data);
|
if (data?.message?.content) {
|
||||||
|
|
||||||
if (data.message && data.message.content) {
|
|
||||||
return { success: true, data: 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) {
|
} catch (error) {
|
||||||
console.error("[Ollama] Chat completion error:", error);
|
console.error("[Ollama] Chat completion error:", error);
|
||||||
return {
|
return {
|
||||||
@@ -85,32 +128,31 @@ export class OllamaCloudService {
|
|||||||
|
|
||||||
async listModels(): Promise<APIResponse<string[]>> {
|
async listModels(): Promise<APIResponse<string[]>> {
|
||||||
try {
|
try {
|
||||||
if (this.config.apiKey) {
|
const response = await fetch(LOCAL_MODELS_URL, {
|
||||||
console.log("[Ollama] Listing models from:", `${this.config.endpoint}/tags`);
|
headers: this.getHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(`${this.config.endpoint}/tags`, {
|
if (!response.ok) {
|
||||||
headers: this.getHeaders(),
|
const errorBody = await response.text();
|
||||||
});
|
throw new Error(`List models failed: ${response.statusText} - ${errorBody}`);
|
||||||
|
|
||||||
console.log("[Ollama] List models response status:", response.status, response.statusText);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to list models: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log("[Ollama] Models data:", data);
|
|
||||||
const models = data.models?.map((m: OllamaModel) => m.name) || [];
|
|
||||||
|
|
||||||
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"] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Ollama] listModels error:", error);
|
console.error("[Ollama] listModels error:", error);
|
||||||
|
if (DEFAULT_MODELS.length > 0) {
|
||||||
|
this.availableModels = DEFAULT_MODELS;
|
||||||
|
return { success: true, data: DEFAULT_MODELS };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Failed to list models",
|
error: error instanceof Error ? error.message : "Failed to list models",
|
||||||
@@ -119,9 +161,7 @@ export class OllamaCloudService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAvailableModels(): string[] {
|
getAvailableModels(): string[] {
|
||||||
return this.availableModels.length > 0
|
return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS;
|
||||||
? this.availableModels
|
|
||||||
: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
|
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
|
||||||
@@ -136,7 +176,7 @@ Apply these principles:
|
|||||||
4. Include edge cases and error handling requirements
|
4. Include edge cases and error handling requirements
|
||||||
5. Specify testing and validation criteria
|
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 = {
|
const userMessage: ChatMessage = {
|
||||||
@@ -198,6 +238,589 @@ Include specific recommendations for:
|
|||||||
|
|
||||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
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;
|
export default OllamaCloudService;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import type { ChatMessage, APIResponse } from "@/types";
|
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||||
|
|
||||||
export interface ZaiPlanConfig {
|
export interface ZaiPlanConfig {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@@ -17,6 +17,10 @@ export class ZaiPlanService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasAuth(): boolean {
|
||||||
|
return !!this.config.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
private getHeaders(): Record<string, string> {
|
private getHeaders(): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -59,7 +63,7 @@ export class ZaiPlanService {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("[Z.AI] Response data:", data);
|
console.log("[Z.AI] Response data:", data);
|
||||||
|
|
||||||
if (data.choices && data.choices[0] && data.choices[0].message) {
|
if (data.choices && data.choices[0] && data.choices[0].message) {
|
||||||
return { success: true, data: data.choices[0].message.content };
|
return { success: true, data: data.choices[0].message.content };
|
||||||
} else if (data.output && data.output.choices && data.output.choices[0]) {
|
} else if (data.output && data.output.choices && data.output.choices[0]) {
|
||||||
@@ -164,7 +168,7 @@ Include specific recommendations for:
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const models = data.data?.map((m: any) => m.id) || [];
|
const models = data.data?.map((m: any) => m.id) || [];
|
||||||
|
|
||||||
return { success: true, data: models };
|
return { success: true, data: models };
|
||||||
} else {
|
} else {
|
||||||
console.log("[Z.AI] No API key, using fallback models");
|
console.log("[Z.AI] No API key, using fallback models");
|
||||||
@@ -182,6 +186,701 @@ Include specific recommendations for:
|
|||||||
getAvailableModels(): string[] {
|
getAvailableModels(): string[] {
|
||||||
return ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"];
|
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;
|
export default ZaiPlanService;
|
||||||
|
|||||||
41
lib/store.ts
41
lib/store.ts
@@ -1,11 +1,17 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan } from "@/types";
|
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult, MarketResearchResult, AppView, AIAssistMessage } from "@/types";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentPrompt: string;
|
currentPrompt: string;
|
||||||
enhancedPrompt: string | null;
|
enhancedPrompt: string | null;
|
||||||
prd: PRD | null;
|
prd: PRD | null;
|
||||||
actionPlan: ActionPlan | null;
|
actionPlan: ActionPlan | null;
|
||||||
|
slidesPresentation: SlidesPresentation | null;
|
||||||
|
googleAdsResult: GoogleAdsResult | null;
|
||||||
|
magicWandResult: MagicWandResult | null;
|
||||||
|
marketResearchResult: MarketResearchResult | null;
|
||||||
|
aiAssistHistory: AIAssistMessage[];
|
||||||
|
language: "en" | "ru" | "he";
|
||||||
selectedProvider: ModelProvider;
|
selectedProvider: ModelProvider;
|
||||||
selectedModels: Record<ModelProvider, string>;
|
selectedModels: Record<ModelProvider, string>;
|
||||||
availableModels: Record<ModelProvider, string[]>;
|
availableModels: Record<ModelProvider, string[]>;
|
||||||
@@ -14,7 +20,7 @@ interface AppState {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
};
|
} | null;
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
history: {
|
history: {
|
||||||
@@ -27,11 +33,17 @@ interface AppState {
|
|||||||
setEnhancedPrompt: (enhanced: string | null) => void;
|
setEnhancedPrompt: (enhanced: string | null) => void;
|
||||||
setPRD: (prd: PRD) => void;
|
setPRD: (prd: PRD) => void;
|
||||||
setActionPlan: (plan: ActionPlan) => void;
|
setActionPlan: (plan: ActionPlan) => void;
|
||||||
|
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||||
|
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||||
|
setMagicWandResult: (result: MagicWandResult | null) => void;
|
||||||
|
setMarketResearchResult: (result: MarketResearchResult | null) => void;
|
||||||
|
setAIAssistHistory: (history: AIAssistMessage[] | ((prev: AIAssistMessage[]) => AIAssistMessage[])) => void;
|
||||||
|
setLanguage: (lang: "en" | "ru" | "he") => void;
|
||||||
setSelectedProvider: (provider: ModelProvider) => void;
|
setSelectedProvider: (provider: ModelProvider) => void;
|
||||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||||
setApiKey: (provider: ModelProvider, key: string) => void;
|
setApiKey: (provider: ModelProvider, key: string) => void;
|
||||||
setQwenTokens: (tokens: { accessToken: string; refreshToken?: string; expiresAt?: number }) => void;
|
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
|
||||||
setProcessing: (processing: boolean) => void;
|
setProcessing: (processing: boolean) => void;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
addToHistory: (prompt: string) => void;
|
addToHistory: (prompt: string) => void;
|
||||||
@@ -44,14 +56,20 @@ const useStore = create<AppState>((set) => ({
|
|||||||
enhancedPrompt: null,
|
enhancedPrompt: null,
|
||||||
prd: null,
|
prd: null,
|
||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
|
slidesPresentation: null,
|
||||||
|
googleAdsResult: null,
|
||||||
|
magicWandResult: null,
|
||||||
|
marketResearchResult: null,
|
||||||
|
aiAssistHistory: [],
|
||||||
|
language: "en",
|
||||||
selectedProvider: "qwen",
|
selectedProvider: "qwen",
|
||||||
selectedModels: {
|
selectedModels: {
|
||||||
qwen: "qwen-coder-plus",
|
qwen: "coder-model",
|
||||||
ollama: "gpt-oss:120b",
|
ollama: "gpt-oss:120b",
|
||||||
zai: "glm-4.7",
|
zai: "glm-4.7",
|
||||||
},
|
},
|
||||||
availableModels: {
|
availableModels: {
|
||||||
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"],
|
qwen: ["coder-model"],
|
||||||
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
|
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
|
||||||
zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||||
},
|
},
|
||||||
@@ -68,6 +86,14 @@ const useStore = create<AppState>((set) => ({
|
|||||||
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
||||||
setPRD: (prd) => set({ prd }),
|
setPRD: (prd) => set({ prd }),
|
||||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||||
|
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||||
|
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||||
|
setMagicWandResult: (result) => set({ magicWandResult: result }),
|
||||||
|
setMarketResearchResult: (result) => set({ marketResearchResult: result }),
|
||||||
|
setAIAssistHistory: (update) => set((state) => ({
|
||||||
|
aiAssistHistory: typeof update === 'function' ? update(state.aiAssistHistory) : update
|
||||||
|
})),
|
||||||
|
setLanguage: (lang) => set({ language: lang }),
|
||||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||||
setSelectedModel: (provider, model) =>
|
setSelectedModel: (provider, model) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -102,6 +128,11 @@ const useStore = create<AppState>((set) => ({
|
|||||||
enhancedPrompt: null,
|
enhancedPrompt: null,
|
||||||
prd: null,
|
prd: null,
|
||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
|
slidesPresentation: null,
|
||||||
|
googleAdsResult: null,
|
||||||
|
magicWandResult: null,
|
||||||
|
marketResearchResult: null,
|
||||||
|
aiAssistHistory: [],
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
basePath: '/tools/promptarch',
|
||||||
|
trailingSlash: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
683
package-lock.json
generated
683
package-lock.json
generated
@@ -9,15 +9,17 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-next": "^15.0.3",
|
"eslint-config-next": "^15.0.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "^15.0.3",
|
"next": "^16.1.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -50,17 +52,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||||
@@ -71,16 +62,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"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"
|
"@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": {
|
"node_modules/@next/env": {
|
||||||
"version": "15.5.9",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
|
||||||
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
|
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -792,9 +761,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
|
||||||
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
|
"integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -808,9 +777,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
|
||||||
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
|
"integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -824,9 +793,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
|
||||||
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
|
"integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -840,9 +809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
|
||||||
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
|
"integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -856,9 +825,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
|
||||||
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
|
"integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -872,9 +841,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
|
||||||
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
|
"integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -888,9 +857,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
|
||||||
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
|
"integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -904,9 +873,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "15.5.7",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
|
||||||
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
|
"integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -963,6 +932,294 @@
|
|||||||
"node": ">=12.4.0"
|
"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": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.11.2",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
@@ -1032,16 +1289,6 @@
|
|||||||
"tslib": "^2.8.0"
|
"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": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
@@ -1188,7 +1435,7 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
@@ -1467,243 +1714,6 @@
|
|||||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/@unrs/resolver-binding-win32-x64-msvc": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
|
"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": ">= 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": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
@@ -3499,20 +3521,6 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -5560,13 +5568,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "15.5.9",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
|
||||||
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
|
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.5.9",
|
"@next/env": "16.1.1",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
|
"baseline-browser-mapping": "^2.8.3",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"styled-jsx": "5.1.6"
|
"styled-jsx": "5.1.6"
|
||||||
@@ -5575,18 +5584,18 @@
|
|||||||
"next": "dist/bin/next"
|
"next": "dist/bin/next"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
"node": ">=20.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "15.5.7",
|
"@next/swc-darwin-arm64": "16.1.1",
|
||||||
"@next/swc-darwin-x64": "15.5.7",
|
"@next/swc-darwin-x64": "16.1.1",
|
||||||
"@next/swc-linux-arm64-gnu": "15.5.7",
|
"@next/swc-linux-arm64-gnu": "16.1.1",
|
||||||
"@next/swc-linux-arm64-musl": "15.5.7",
|
"@next/swc-linux-arm64-musl": "16.1.1",
|
||||||
"@next/swc-linux-x64-gnu": "15.5.7",
|
"@next/swc-linux-x64-gnu": "16.1.1",
|
||||||
"@next/swc-linux-x64-musl": "15.5.7",
|
"@next/swc-linux-x64-musl": "16.1.1",
|
||||||
"@next/swc-win32-arm64-msvc": "15.5.7",
|
"@next/swc-win32-arm64-msvc": "16.1.1",
|
||||||
"@next/swc-win32-x64-msvc": "15.5.7",
|
"@next/swc-win32-x64-msvc": "16.1.1",
|
||||||
"sharp": "^0.34.3"
|
"sharp": "^0.34.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@opentelemetry/api": "^1.1.0",
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -9,15 +9,17 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-next": "^15.0.3",
|
"eslint-config-next": "^15.0.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "^15.0.3",
|
"next": "^16.1.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -37,7 +39,23 @@
|
|||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.2"
|
"@types/react-dom": "^19.0.2"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [
|
||||||
"author": "",
|
"ai",
|
||||||
"license": "ISC"
|
"prompt-engineering",
|
||||||
|
"prd-generator",
|
||||||
|
"nextjs",
|
||||||
|
"qwen",
|
||||||
|
"ollama",
|
||||||
|
"zai"
|
||||||
|
],
|
||||||
|
"author": "Roman | RyzenAdvanced <https://github.com/roman-ryzenadvanced>",
|
||||||
|
"license": "ISC",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -11,7 +15,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -19,9 +23,19 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
214
types/index.ts
214
types/index.ts
@@ -91,3 +91,217 @@ export interface ChatMessage {
|
|||||||
role: "system" | "user" | "assistant";
|
role: "system" | "user" | "assistant";
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Slide {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
htmlContent: string;
|
||||||
|
notes?: string;
|
||||||
|
layout: "title" | "content" | "two-column" | "image-left" | "image-right" | "quote" | "statistics" | "timeline" | "comparison";
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlidesPresentation {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
author?: string;
|
||||||
|
organization?: string;
|
||||||
|
theme: "corporate" | "modern" | "minimal" | "dark" | "vibrant" | "gradient";
|
||||||
|
language: string;
|
||||||
|
slides: Slide[];
|
||||||
|
rawContent: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleAdsKeyword {
|
||||||
|
keyword: string;
|
||||||
|
type: "primary" | "long-tail" | "negative";
|
||||||
|
searchVolume?: number;
|
||||||
|
competition: "low" | "medium" | "high";
|
||||||
|
difficultyScore?: number;
|
||||||
|
relevanceScore?: number;
|
||||||
|
cpc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleAdCopy {
|
||||||
|
id: string;
|
||||||
|
campaignType: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||||
|
headlines: string[];
|
||||||
|
descriptions: string[];
|
||||||
|
callToAction: string;
|
||||||
|
displayUrl?: string;
|
||||||
|
finalUrl?: string;
|
||||||
|
mobileOptimized: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleAdGroup {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
theme: string;
|
||||||
|
keywords: string[];
|
||||||
|
ads: GoogleAdCopy[];
|
||||||
|
biddingStrategy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleAdsCampaign {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||||
|
budget: {
|
||||||
|
daily?: number;
|
||||||
|
monthly?: number;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
targeting: {
|
||||||
|
locations?: string[];
|
||||||
|
demographics?: 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";
|
||||||
|
|||||||
@@ -2,5 +2,8 @@
|
|||||||
"buildCommand": "npm run build",
|
"buildCommand": "npm run build",
|
||||||
"outputDirectory": ".next",
|
"outputDirectory": ".next",
|
||||||
"framework": "nextjs",
|
"framework": "nextjs",
|
||||||
"devCommand": "npm run dev"
|
"devCommand": "npm run dev",
|
||||||
}
|
"env": {
|
||||||
|
"NEXT_PUBLIC_SITE_URL": "https://traetlzlxn2t.vercel.app"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user