Compare commits
24 Commits
remove-qwe
...
8733b885d4
@@ -14,3 +14,7 @@ OLLAMA_ENDPOINT=https://ollama.com/api
|
||||
ZAI_API_KEY=
|
||||
ZAI_GENERAL_ENDPOINT=https://api.z.ai/api/paas/v4
|
||||
ZAI_CODING_ENDPOINT=https://api.z.ai/api/coding/paas/v4
|
||||
|
||||
# Site Configuration (Required for OAuth in production)
|
||||
# Set to your production URL (e.g., https://your-app.vercel.app)
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:6002
|
||||
|
||||
15
LICENSE
Normal file
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 🚀
|
||||
|
||||
> **Development Note**: This entire platform was developed exclusively using the [TRAE.AI IDE](https://trae.ai) powered by the elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
|
||||
> **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
|
||||
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
|
||||
|
||||
---
|
||||
|
||||
> **Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
|
||||
> **Fork Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
|
||||
|
||||
Transform vague ideas into production-ready prompts and PRDs. PromptArch is an elite AI orchestration platform designed for software architects and Vibe Coders.
|
||||
|
||||
**Developed by [Roman | RyzenAdvanced](https://github.com/roman-ryzenadvanced)**
|
||||
|
||||
- 📦 **GitHub Repository**: [roman-ryzenadvanced/PromptArch-the-prompt-enhancer](https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer)
|
||||
- 📮 **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||
|
||||
## 🌟 Visual Overview
|
||||
|
||||
### 🛠 Core Capabilities
|
||||
@@ -56,21 +61,20 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an e
|
||||
- **Components**: [shadcn/ui](https://ui.shadcn.com/)
|
||||
- **Icons**: [Lucide React](https://lucide.dev/)
|
||||
|
||||
## 🤝 Attribution
|
||||
## 🤝 Attribution & Credits
|
||||
|
||||
This project is a visual and architectural evolution of the [Clavix](https://github.com/ClavixDev/Clavix) framework. While Clavix focuses on agentic-first Markdown templates, PromptArch provides a centralized web interface to execute these workflows with advanced model orchestration.
|
||||
**Author**: [Roman | RyzenAdvanced](https://github.com/roman-ryzenadvanced)
|
||||
- 📦 **GitHub**: [roman-ryzenadvanced/PromptArch-the-prompt-enhancer](https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer)
|
||||
- 📮 **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||
|
||||
Developed by **Roman | RyzenAdvanced**
|
||||
- GitHub: [roman-ryzenadvanced](https://github.com/roman-ryzenadvanced)
|
||||
- Telegram: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
|
||||
**Forked from**: [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix)
|
||||
- This project is a visual and architectural evolution of the Clavix framework
|
||||
- Clavix focuses on agentic-first Markdown templates
|
||||
- PromptArch provides a centralized web interface with advanced model orchestration
|
||||
|
||||
---
|
||||
*100% Developed using GLM 4.7 model on TRAE.AI IDE.*
|
||||
- **Styling**: TailwindCSS
|
||||
- **UI Components**: shadcn/ui + Radix UI
|
||||
- **State Management**: Zustand
|
||||
- **Forms**: React Hook Form + Zod
|
||||
- **Icons**: Lucide React
|
||||
**Development Platform**: [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW)
|
||||
- 100% AI-assisted development using TRAE.AI's advanced coding capabilities
|
||||
- Learn more about the architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW)
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
57
app/api/ollama/chat/route.ts
Normal file
57
app/api/ollama/chat/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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),
|
||||
});
|
||||
|
||||
const payload = await response.text();
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Ollama chat request failed", details: payload },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(payload ? JSON.parse(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 }
|
||||
);
|
||||
}
|
||||
70
app/api/qwen/chat/route.ts
Normal file
70
app/api/qwen/chat/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.text();
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: payload || response.statusText || "Qwen chat failed" },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(payload);
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: payload || "Unexpected response format" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
} 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,71 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Better touch targets */
|
||||
button,
|
||||
a,
|
||||
[role="button"] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Prevent text selection on buttons */
|
||||
button {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Safe area padding for notched devices */
|
||||
.safe-area-inset {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Scrollbar styling for mobile-like experience */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes progress-indeterminate {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-progress-indeterminate {
|
||||
animation: progress-indeterminate 1.5s infinite linear;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Roboto } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const roboto = Roboto({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "700"]
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PromptArch - AI Prompt Engineering Platform",
|
||||
description: "Transform vague ideas into production-ready prompts and PRDs",
|
||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -16,7 +20,7 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<body className={roboto.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
60
app/page.tsx
60
app/page.tsx
@@ -6,60 +6,19 @@ import type { View } from "@/components/Sidebar";
|
||||
import PromptEnhancer from "@/components/PromptEnhancer";
|
||||
import PRDGenerator from "@/components/PRDGenerator";
|
||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
||||
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
||||
import SlidesGenerator from "@/components/SlidesGenerator";
|
||||
import GoogleAdsGenerator from "@/components/GoogleAdsGenerator";
|
||||
import HistoryPanel from "@/components/HistoryPanel";
|
||||
import SettingsPanel from "@/components/SettingsPanel";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
|
||||
export default function Home() {
|
||||
const [currentView, setCurrentView] = useState<View>("enhance");
|
||||
const { setQwenTokens, setApiKey } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Handle OAuth callback
|
||||
if (typeof window !== "undefined") {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get("code");
|
||||
|
||||
if (code) {
|
||||
// In a real app, you would exchange the code for tokens here
|
||||
// Since we don't have a backend or real client secret, we'll simulate it
|
||||
console.log("OAuth code received:", code);
|
||||
|
||||
// Mock token exchange
|
||||
const mockAccessToken = "mock_access_token_" + Math.random().toString(36).substr(2, 9);
|
||||
const tokens = {
|
||||
accessToken: mockAccessToken,
|
||||
expiresAt: Date.now() + 3600 * 1000, // 1 hour
|
||||
};
|
||||
|
||||
setQwenTokens(tokens);
|
||||
modelAdapter.setQwenOAuthTokens(tokens.accessToken, undefined, 3600);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem("promptarch-qwen-tokens", JSON.stringify(tokens));
|
||||
|
||||
// Clear the code from URL
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
|
||||
// Switch to settings to show success (optional)
|
||||
setCurrentView("settings");
|
||||
}
|
||||
|
||||
// Load tokens from localStorage on init
|
||||
const savedTokens = localStorage.getItem("promptarch-qwen-tokens");
|
||||
if (savedTokens) {
|
||||
try {
|
||||
const tokens = JSON.parse(savedTokens);
|
||||
if (tokens.expiresAt > Date.now()) {
|
||||
setQwenTokens(tokens);
|
||||
modelAdapter.setQwenOAuthTokens(tokens.accessToken, tokens.refreshToken, (tokens.expiresAt - Date.now()) / 1000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load Qwen tokens:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("[Home] Initializing Qwen OAuth service on client...");
|
||||
modelAdapter["qwenService"]["initialize"]?.();
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
@@ -70,6 +29,12 @@ export default function Home() {
|
||||
return <PRDGenerator />;
|
||||
case "action":
|
||||
return <ActionPlanGenerator />;
|
||||
case "uxdesigner":
|
||||
return <UXDesignerPrompt />;
|
||||
case "slides":
|
||||
return <SlidesGenerator />;
|
||||
case "googleads":
|
||||
return <GoogleAdsGenerator />;
|
||||
case "history":
|
||||
return <HistoryPanel />;
|
||||
case "settings":
|
||||
@@ -82,7 +47,7 @@ export default function Home() {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
||||
<Sidebar currentView={currentView} onViewChange={setCurrentView} />
|
||||
<main className="flex-1 overflow-auto p-8">
|
||||
<main className="flex-1 overflow-auto pt-16 lg:pt-0 px-4 py-4 lg:p-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
{renderContent()}
|
||||
</div>
|
||||
@@ -90,3 +55,4 @@ export default function Home() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,9 @@ export default function ActionPlanGenerator() {
|
||||
}
|
||||
|
||||
const apiKey = apiKeys[selectedProvider];
|
||||
if (!apiKey || !apiKey.trim()) {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
return;
|
||||
}
|
||||
@@ -79,9 +81,13 @@ export default function ActionPlanGenerator() {
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
|
||||
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
|
||||
|
||||
console.log("[ActionPlanGenerator] Generation result:", result);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const newPlan = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
@@ -100,9 +106,11 @@ export default function ActionPlanGenerator() {
|
||||
};
|
||||
setActionPlan(newPlan);
|
||||
} else {
|
||||
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate action plan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[ActionPlanGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
@@ -118,28 +126,28 @@ export default function ActionPlanGenerator() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ListTodo className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Action Plan Generator
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Convert PRD into actionable implementation plan
|
||||
</CardDescription>
|
||||
</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">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex gap-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize"
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
@@ -148,11 +156,11 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
@@ -163,36 +171,36 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">PRD / Requirements</label>
|
||||
<label className="text-xs lg:text-sm font-medium">PRD / Requirements</label>
|
||||
<Textarea
|
||||
placeholder="Paste your PRD or project requirements here..."
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[200px] resize-y"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="text-xs">Configure API key in Settings</span>
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating Action Plan...
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
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
|
||||
</>
|
||||
)}
|
||||
@@ -201,43 +209,43 @@ export default function ActionPlanGenerator() {
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!actionPlan && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Action Plan
|
||||
</span>
|
||||
{actionPlan && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Task breakdown, frameworks, and architecture recommendations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{actionPlan ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border bg-primary/5 p-4">
|
||||
<h4 className="mb-2 flex items-center gap-2 font-semibold">
|
||||
<Clock className="h-4 w-4" />
|
||||
<div className="space-y-3 lg:space-y-4">
|
||||
<div className="rounded-md border bg-primary/5 p-3 lg:p-4">
|
||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Implementation Roadmap
|
||||
</h4>
|
||||
<pre className="whitespace-pre-wrap text-sm">{actionPlan.rawContent}</pre>
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{actionPlan.rawContent}</pre>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/30 p-4">
|
||||
<h4 className="mb-2 flex items-center gap-2 font-semibold">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Quick Notes
|
||||
</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>Set up recommended framework architecture</li>
|
||||
<li>Follow best practices for security and performance</li>
|
||||
@@ -246,7 +254,7 @@ export default function ActionPlanGenerator() {
|
||||
</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">
|
||||
Action plan will appear here
|
||||
</div>
|
||||
)}
|
||||
|
||||
533
components/GoogleAdsGenerator.tsx
Normal file
533
components/GoogleAdsGenerator.tsx
Normal file
@@ -0,0 +1,533 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GoogleAdsResult } from "@/types";
|
||||
|
||||
export default function GoogleAdsGenerator() {
|
||||
const {
|
||||
googleAdsResult,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
setGoogleAdsResult,
|
||||
setProcessing,
|
||||
setError,
|
||||
setAvailableModels,
|
||||
setSelectedModel,
|
||||
setSelectedProvider,
|
||||
} = useStore();
|
||||
|
||||
// Input states
|
||||
const [websiteUrl, setWebsiteUrl] = useState("");
|
||||
const [products, setProducts] = useState<string[]>([""]);
|
||||
const [targetAudience, setTargetAudience] = useState("");
|
||||
const [budgetMin, setBudgetMin] = useState("500");
|
||||
const [budgetMax, setBudgetMax] = useState("2000");
|
||||
const [duration, setDuration] = useState("30 days");
|
||||
const [industry, setIndustry] = useState("");
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [expandedSections, setExpandedSections] = useState<string[]>(["keywords"]);
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections((prev) =>
|
||||
prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section]
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
const saved = localStorage.getItem("promptarch-api-keys");
|
||||
if (saved) {
|
||||
try {
|
||||
const keys = JSON.parse(saved);
|
||||
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||
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 addProduct = () => setProducts([...products, ""]);
|
||||
const removeProduct = (index: number) => {
|
||||
const newProducts = products.filter((_, i) => i !== index);
|
||||
setProducts(newProducts.length ? newProducts : [""]);
|
||||
};
|
||||
const updateProduct = (index: number, value: string) => {
|
||||
const newProducts = [...products];
|
||||
newProducts[index] = value;
|
||||
setProducts(newProducts);
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!websiteUrl.trim()) {
|
||||
setError("Please enter a website URL");
|
||||
return;
|
||||
}
|
||||
const filteredProducts = products.filter(p => p.trim() !== "");
|
||||
if (filteredProducts.length === 0) {
|
||||
setError("Please add at least one product or service");
|
||||
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);
|
||||
|
||||
console.log("[GoogleAdsGenerator] Starting generation...", { selectedProvider, selectedModel });
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.generateGoogleAds(websiteUrl, {
|
||||
productsServices: filteredProducts,
|
||||
targetAudience,
|
||||
budgetRange: { min: parseInt(budgetMin), max: parseInt(budgetMax), currency: "USD" },
|
||||
campaignDuration: duration,
|
||||
industry,
|
||||
language: "English"
|
||||
}, selectedProvider, selectedModel);
|
||||
|
||||
console.log("[GoogleAdsGenerator] Generation result:", result);
|
||||
|
||||
if (result.success && result.data) {
|
||||
try {
|
||||
// Robust JSON extraction
|
||||
const extractJson = (text: string) => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/i) ||
|
||||
text.match(/```\s*([\s\S]*?)\s*```/i);
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
try {
|
||||
return JSON.parse(jsonMatch[1].trim());
|
||||
} catch (e2) { /* ignore */ }
|
||||
}
|
||||
const braceMatch = text.match(/(\{[\s\S]*\})/);
|
||||
if (braceMatch) {
|
||||
try {
|
||||
return JSON.parse(braceMatch[0].trim());
|
||||
} catch (e3) { /* ignore */ }
|
||||
}
|
||||
throw new Error("Could not parse JSON from response");
|
||||
}
|
||||
};
|
||||
|
||||
const rawData = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
|
||||
const parsedData = extractJson(rawData);
|
||||
|
||||
const adsResult: GoogleAdsResult = {
|
||||
...parsedData,
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
websiteUrl,
|
||||
productsServices: filteredProducts,
|
||||
generatedAt: new Date(),
|
||||
rawContent: rawData
|
||||
};
|
||||
setGoogleAdsResult(adsResult);
|
||||
setExpandedSections(["keywords"]);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ads data:", e);
|
||||
setError("Failed to parse the generated ads content. Please try again.");
|
||||
}
|
||||
} else {
|
||||
console.error("[GoogleAdsGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate Google Ads campaign");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[GoogleAdsGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (googleAdsResult?.rawContent) {
|
||||
await navigator.clipboard.writeText(googleAdsResult.rawContent);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{ id: "keywords", title: "Keywords Research" },
|
||||
{ id: "adcopies", title: "Ad Copy Variations" },
|
||||
{ id: "campaigns", title: "Campaign Structure" },
|
||||
{ id: "implementation", title: "Implementation Guide" },
|
||||
];
|
||||
|
||||
const renderSectionContent = (sectionId: string) => {
|
||||
if (!googleAdsResult) return null;
|
||||
|
||||
switch (sectionId) {
|
||||
case "keywords":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{googleAdsResult.keywords?.primary?.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Primary Keywords</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{googleAdsResult.keywords.primary.map((k, i) => (
|
||||
<span key={i} className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-md">
|
||||
{k.keyword} {k.cpc && <span className="opacity-60">({k.cpc})</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{googleAdsResult.keywords?.longTail?.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Long-Tail Keywords</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{googleAdsResult.keywords.longTail.map((k, i) => (
|
||||
<span key={i} className="text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded-md">
|
||||
{k.keyword}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{googleAdsResult.keywords?.negative?.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Negative Keywords</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{googleAdsResult.keywords.negative.map((k, i) => (
|
||||
<span key={i} className="text-xs bg-red-50 text-red-700 px-2 py-1 rounded-md line-through">
|
||||
{k.keyword}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case "adcopies":
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{googleAdsResult.adCopies?.map((ad, i) => (
|
||||
<div key={i} className="p-3 rounded-md border bg-muted/20">
|
||||
<div className="text-[10px] uppercase text-muted-foreground mb-2">Ad Variation {i + 1}</div>
|
||||
<div className="space-y-1 mb-2">
|
||||
{ad.headlines?.map((h, j) => (
|
||||
<div key={j} className="text-sm font-medium text-blue-600">{h}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{ad.descriptions?.map((d, j) => (
|
||||
<p key={j} className="text-xs text-muted-foreground">{d}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
case "campaigns":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{googleAdsResult.campaigns?.map((camp, i) => (
|
||||
<div key={i} className="p-3 rounded-md border">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div className="font-medium text-sm">{camp.name}</div>
|
||||
<div className="text-[10px] text-muted-foreground uppercase">{camp.type}</div>
|
||||
</div>
|
||||
{camp.budget && (
|
||||
<div className="text-right text-xs">
|
||||
<div className="font-semibold">${camp.budget.monthly}/mo</div>
|
||||
<div className="text-muted-foreground">${camp.budget.daily}/day</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{camp.adGroups?.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t">
|
||||
<div className="text-[10px] uppercase text-muted-foreground mb-1">Ad Groups</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{camp.adGroups.map((g, j) => (
|
||||
<span key={j} className="text-[10px] bg-muted px-1.5 py-0.5 rounded">{g.name}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
case "implementation":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{googleAdsResult.implementation?.setupSteps?.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Setup Steps</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
{googleAdsResult.implementation.setupSteps.map((step, i) => (
|
||||
<li key={i}>{step}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
{googleAdsResult.implementation?.qualityScoreTips?.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Quality Score Tips</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-xs">
|
||||
{googleAdsResult.implementation.qualityScoreTips.map((tip, i) => (
|
||||
<li key={i}>{tip}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <pre className="whitespace-pre-wrap text-xs">{googleAdsResult.rawContent}</pre>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Google Ads Generator
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Generate keywords, ad copy, and campaign structure for Google Ads
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Website URL</label>
|
||||
<Input
|
||||
placeholder="e.g., www.your-business.com"
|
||||
value={websiteUrl}
|
||||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Products / Services</label>
|
||||
<div className="space-y-2">
|
||||
{products.map((product, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={`Product ${index + 1}`}
|
||||
value={product}
|
||||
onChange={(e) => updateProduct(index, e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
{products.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeProduct(index)}
|
||||
className="h-10 w-10 shrink-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
Add Product
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Budget (USD/mo)</label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
value={budgetMin}
|
||||
onChange={(e) => setBudgetMin(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">-</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={budgetMax}
|
||||
onChange={(e) => setBudgetMax(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Industry</label>
|
||||
<Input
|
||||
placeholder="e.g., SaaS"
|
||||
value={industry}
|
||||
onChange={(e) => setIndustry(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Target Audience</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Small business owners in USA looking for productivity tools"
|
||||
value={targetAudience}
|
||||
onChange={(e) => setTargetAudience(e.target.value)}
|
||||
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !websiteUrl.trim()} className="w-full 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" />
|
||||
Generating Ads...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate Google Ads
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!googleAdsResult && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Generated Campaign
|
||||
</span>
|
||||
{googleAdsResult && (
|
||||
<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">
|
||||
Keywords, ad copy, and campaign structure ready for Google Ads
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{googleAdsResult ? (
|
||||
<div className="space-y-2 lg:space-y-3">
|
||||
{sections.map((section) => (
|
||||
<div key={section.id} className="rounded-md border bg-muted/30">
|
||||
<button
|
||||
onClick={() => toggleSection(section.id)}
|
||||
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
||||
>
|
||||
<span>{section.title}</span>
|
||||
{expandedSections.includes(section.id) ? (
|
||||
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</button>
|
||||
{expandedSections.includes(section.id) && (
|
||||
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3">
|
||||
{renderSectionContent(section.id)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||
Generated campaign will appear here
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import useStore from "@/lib/store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function HistoryPanel() {
|
||||
const { history, setCurrentPrompt, clearHistory } = useStore();
|
||||
@@ -22,11 +21,11 @@ export default function HistoryPanel() {
|
||||
if (history.length === 0) {
|
||||
return (
|
||||
<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">
|
||||
<div className="text-center">
|
||||
<Clock className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="mt-4 text-muted-foreground">No history yet</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
||||
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground">No history yet</p>
|
||||
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
||||
Start enhancing prompts to see them here
|
||||
</p>
|
||||
</div>
|
||||
@@ -37,35 +36,35 @@ export default function HistoryPanel() {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6">
|
||||
<div>
|
||||
<CardTitle>History</CardTitle>
|
||||
<CardDescription>{history.length} items</CardDescription>
|
||||
<CardTitle className="text-base lg:text-lg">History</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">{history.length} items</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleClear}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CardContent className="space-y-2 lg:space-y-3 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{history.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="rounded-md border bg-muted/30 p-4 transition-colors hover:bg-muted/50"
|
||||
className="rounded-md border bg-muted/30 p-3 lg:p-4 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<div className="mb-1.5 lg:mb-2 flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] lg:text-xs text-muted-foreground truncate">
|
||||
{new Date(item.timestamp).toLocaleString()}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
onClick={() => handleRestore(item.prompt)}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="line-clamp-3 text-sm">{item.prompt}</p>
|
||||
<p className="line-clamp-3 text-xs lg:text-sm">{item.prompt}</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
|
||||
@@ -78,7 +78,9 @@ export default function PRDGenerator() {
|
||||
}
|
||||
|
||||
const apiKey = apiKeys[selectedProvider];
|
||||
if (!apiKey || !apiKey.trim()) {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
return;
|
||||
}
|
||||
@@ -86,9 +88,13 @@ export default function PRDGenerator() {
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
|
||||
console.log("[PRDGenerator] Starting PRD generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel);
|
||||
|
||||
console.log("[PRDGenerator] Generation result:", result);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const newPRD = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
@@ -105,9 +111,11 @@ export default function PRDGenerator() {
|
||||
};
|
||||
setPRD(newPRD);
|
||||
} else {
|
||||
console.error("[PRDGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate PRD");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[PRDGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
@@ -132,28 +140,28 @@ export default function PRDGenerator() {
|
||||
];
|
||||
|
||||
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">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
PRD Generator
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Generate comprehensive Product Requirements Document from your idea
|
||||
</CardDescription>
|
||||
</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">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex gap-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize"
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
@@ -162,11 +170,11 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
@@ -177,36 +185,36 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Your Idea</label>
|
||||
<label className="text-xs lg:text-sm font-medium">Your Idea</label>
|
||||
<Textarea
|
||||
placeholder="e.g., A task management app with real-time collaboration features"
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[200px] resize-y"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="text-xs">Configure API key in Settings</span>
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Generating PRD...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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
|
||||
</>
|
||||
)}
|
||||
@@ -215,52 +223,52 @@ export default function PRDGenerator() {
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!prd && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Generated PRD
|
||||
</span>
|
||||
{prd && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Structured requirements document ready for development
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{prd ? (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2 lg:space-y-3">
|
||||
{sections.map((section) => (
|
||||
<div key={section.id} className="rounded-md border bg-muted/30">
|
||||
<button
|
||||
onClick={() => toggleSection(section.id)}
|
||||
className="flex w-full items-center justify-between px-4 py-3 text-left font-medium transition-colors hover:bg-muted/50"
|
||||
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
||||
>
|
||||
<span>{section.title}</span>
|
||||
{expandedSections.includes(section.id) ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</button>
|
||||
{expandedSections.includes(section.id) && (
|
||||
<div className="border-t bg-background px-4 py-3">
|
||||
<pre className="whitespace-pre-wrap text-sm">{prd.overview}</pre>
|
||||
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{prd.overview}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground">
|
||||
<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
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -71,7 +71,9 @@ export default function PromptEnhancer() {
|
||||
}
|
||||
|
||||
const apiKey = apiKeys[selectedProvider];
|
||||
if (!apiKey || !apiKey.trim()) {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
return;
|
||||
}
|
||||
@@ -79,15 +81,21 @@ export default function PromptEnhancer() {
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
|
||||
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
|
||||
|
||||
console.log("[PromptEnhancer] Enhancement result:", result);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setEnhancedPrompt(result.data);
|
||||
} else {
|
||||
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
||||
setError(result.error || "Failed to enhance prompt");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[PromptEnhancer] Enhancement error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
@@ -109,21 +117,21 @@ export default function PromptEnhancer() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Input Prompt
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Enter your prompt and we'll enhance it for AI coding agents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-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">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
@@ -131,7 +139,7 @@ export default function PromptEnhancer() {
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={cn(
|
||||
"capitalize",
|
||||
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
>
|
||||
@@ -142,11 +150,11 @@ export default function PromptEnhancer() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
@@ -157,77 +165,77 @@ export default function PromptEnhancer() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Your Prompt</label>
|
||||
<label className="text-xs lg:text-sm font-medium">Your Prompt</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Create a user authentication system with JWT tokens"
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[200px] resize-y"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="text-xs">Configure API key in Settings</span>
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1">
|
||||
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Enhancing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="mr-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
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Clear
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="hidden sm:inline">Clear</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!enhancedPrompt && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Enhanced Prompt
|
||||
</span>
|
||||
{enhancedPrompt && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Professional prompt ready for coding agents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{enhancedPrompt ? (
|
||||
<div className="rounded-md border bg-muted/50 p-4">
|
||||
<pre className="whitespace-pre-wrap text-sm">{enhancedPrompt}</pre>
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{enhancedPrompt}</pre>
|
||||
</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">
|
||||
Enhanced prompt will appear here
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -43,6 +42,10 @@ export default function SettingsPanel() {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
}
|
||||
const storedTokens = modelAdapter.getQwenTokenInfo();
|
||||
if (storedTokens) {
|
||||
setQwenTokens(storedTokens);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,26 +65,49 @@ 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 : "Qwen authentication failed"
|
||||
);
|
||||
} finally {
|
||||
setIsAuthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-6">
|
||||
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Key className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
API Configuration
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Configure API keys for different AI providers
|
||||
</CardDescription>
|
||||
</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">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Server className="h-4 w-4" />
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Qwen Code API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -90,24 +116,24 @@ export default function SettingsPanel() {
|
||||
placeholder="Enter your Qwen API key"
|
||||
value={apiKeys.qwen || ""}
|
||||
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
|
||||
>
|
||||
{showApiKey.qwen ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-xs text-muted-foreground flex-1">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://help.aliyun.com/zh/dashscope/"
|
||||
@@ -121,30 +147,27 @@ export default function SettingsPanel() {
|
||||
<Button
|
||||
variant={qwenTokens ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => {
|
||||
if (qwenTokens) {
|
||||
setQwenTokens(undefined as any);
|
||||
localStorage.removeItem("promptarch-qwen-tokens");
|
||||
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
|
||||
} else {
|
||||
window.location.href = modelAdapter.getQwenAuthUrl();
|
||||
}
|
||||
}}
|
||||
className="h-7 lg:h-8 text-[10px] lg:text-xs w-full sm:w-auto"
|
||||
onClick={handleQwenAuth}
|
||||
disabled={isAuthLoading}
|
||||
>
|
||||
{qwenTokens ? "Logout from Qwen" : "Login with Qwen (OAuth)"}
|
||||
{isAuthLoading
|
||||
? "Signing in..."
|
||||
: qwenTokens
|
||||
? "Logout from Qwen"
|
||||
: "Login with Qwen (OAuth)"}
|
||||
</Button>
|
||||
</div>
|
||||
{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()})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Server className="h-4 w-4" />
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Ollama Cloud API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -153,23 +176,23 @@ export default function SettingsPanel() {
|
||||
placeholder="Enter your Ollama API key"
|
||||
value={apiKeys.ollama || ""}
|
||||
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
|
||||
>
|
||||
{showApiKey.ollama ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://ollama.com/cloud"
|
||||
@@ -183,8 +206,8 @@ export default function SettingsPanel() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Server className="h-4 w-4" />
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Z.AI Plan API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -193,23 +216,23 @@ export default function SettingsPanel() {
|
||||
placeholder="Enter your Z.AI API key"
|
||||
value={apiKeys.zai || ""}
|
||||
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
|
||||
>
|
||||
{showApiKey.zai ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://docs.z.ai"
|
||||
@@ -222,45 +245,44 @@ export default function SettingsPanel() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSave} className="w-full">
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Save API Keys
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Default Provider</CardTitle>
|
||||
<CardDescription>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Default Provider</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Select your preferred AI provider
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3">
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="grid gap-2 lg:gap-3">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<button
|
||||
key={provider}
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 ${
|
||||
selectedProvider === provider
|
||||
className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg:p-4 text-left transition-colors hover:bg-muted/50 ${selectedProvider === provider
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10">
|
||||
<Server className="h-5 w-5 text-primary" />
|
||||
<div className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
|
||||
<Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium capitalize">{provider}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||
{provider === "qwen" && "Alibaba DashScope API"}
|
||||
{provider === "ollama" && "Ollama Cloud API"}
|
||||
{provider === "zai" && "Z.AI Plan API"}
|
||||
</p>
|
||||
</div>
|
||||
{selectedProvider === provider && (
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
<div className="h-2 w-2 rounded-full bg-primary flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -269,15 +291,15 @@ export default function SettingsPanel() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Privacy</CardTitle>
|
||||
<CardDescription>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Data Privacy</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Your data handling preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border bg-muted/30 p-4">
|
||||
<p className="text-sm">
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useStore from "@/lib/store";
|
||||
import { Sparkles, FileText, ListTodo, Settings, History } from "lucide-react";
|
||||
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type View = "enhance" | "prd" | "action" | "history" | "settings";
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "history" | "settings";
|
||||
|
||||
interface SidebarProps {
|
||||
currentView: View;
|
||||
@@ -14,76 +15,143 @@ interface SidebarProps {
|
||||
|
||||
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
const history = useStore((state) => state.history);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const menuItems = [
|
||||
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
|
||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
||||
{ id: "slides" as View, label: "Slides Generator", icon: Presentation },
|
||||
{ id: "googleads" as View, label: "Google Ads Gen", icon: Megaphone },
|
||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="flex h-screen w-64 flex-col border-r bg-card">
|
||||
<div className="border-b p-6">
|
||||
<h1 className="flex items-center gap-2 text-xl font-bold">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||
PA
|
||||
</div>
|
||||
PromptArch
|
||||
</h1>
|
||||
const handleViewChange = (view: View) => {
|
||||
onViewChange(view);
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
const SidebarContent = () => (
|
||||
<>
|
||||
<div className="border-b p-4 lg:p-6">
|
||||
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
<Menu className="h-3 w-3" />
|
||||
Back to rommark.dev
|
||||
</a>
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
|
||||
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
|
||||
<div className="flex h-7 w-7 lg:h-8 lg:w-8 items-center justify-center rounded-lg bg-[#4285F4] text-primary-foreground text-sm lg:text-base">
|
||||
PA
|
||||
</div>
|
||||
PromptArch
|
||||
</h1>
|
||||
</a>
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
|
||||
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
|
||||
<span>View on GitHub</span>
|
||||
</a>
|
||||
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
|
||||
Forked from <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 p-4">
|
||||
<nav className="flex-1 space-y-1 p-3 lg:p-4 overflow-y-auto">
|
||||
{menuItems.map((item) => (
|
||||
<Button
|
||||
key={item.id}
|
||||
variant={currentView === item.id ? "default" : "ghost"}
|
||||
className={cn(
|
||||
"w-full justify-start gap-2",
|
||||
"w-full justify-start gap-2 h-9 lg:h-10 text-sm",
|
||||
currentView === item.id && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
onClick={() => onViewChange(item.id)}
|
||||
onClick={() => handleViewChange(item.id)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span className="flex-1 text-left">{item.label}</span>
|
||||
<span className="flex-1 text-left truncate">{item.label}</span>
|
||||
{item.count !== undefined && item.count > 0 && (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium">
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium text-primary">
|
||||
{item.count}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="mt-8 p-3 text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-4">
|
||||
<div className="mt-6 lg:mt-8 p-2 lg:p-3 text-[9px] lg:text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-3 lg:pt-4">
|
||||
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5 lg:space-y-1">
|
||||
<p>
|
||||
GitHub: <a href="https://github.com/roman-ryzenadvanced/Custom-Engineered-Agents-and-Tools-for-Vibe-Coders" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a>
|
||||
GitHub: <a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a>
|
||||
</p>
|
||||
<p>
|
||||
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
|
||||
</p>
|
||||
<p className="mt-2 text-[9px] opacity-80">
|
||||
<p className="mt-1 lg:mt-2 text-[8px] lg:text-[9px] opacity-80">
|
||||
100% Developed using GLM 4.7 model on TRAE.AI IDE.
|
||||
</p>
|
||||
<p className="text-[9px] opacity-80">
|
||||
<p className="text-[8px] lg:text-[9px] opacity-80">
|
||||
Model Info: <a href="https://z.ai/subscribe?ic=R0K78RJKNW" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Learn here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="border-t p-4">
|
||||
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground">
|
||||
<div className="border-t p-3 lg:p-4 hidden lg:block">
|
||||
<div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
|
||||
<p className="font-medium text-foreground">Quick Tips</p>
|
||||
<ul className="mt-2 space-y-1">
|
||||
<ul className="mt-1.5 lg:mt-2 space-y-0.5 lg:space-y-1">
|
||||
<li>• Use different providers for best results</li>
|
||||
<li>• Copy enhanced prompts to your AI agent</li>
|
||||
<li>• PRDs generate better action plans</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
1291
components/SlidesGenerator.tsx
Normal file
1291
components/SlidesGenerator.tsx
Normal file
File diff suppressed because it is too large
Load Diff
252
components/UXDesignerPrompt.tsx
Normal file
252
components/UXDesignerPrompt.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
"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";
|
||||
|
||||
export default function UXDesignerPrompt() {
|
||||
const {
|
||||
currentPrompt,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
setSelectedProvider,
|
||||
setCurrentPrompt,
|
||||
setEnhancedPrompt,
|
||||
setProcessing,
|
||||
setError,
|
||||
setAvailableModels,
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
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">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
UX Designer Prompt
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Describe your app idea and get the BEST EVER prompt for UX design
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["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">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">App Description</label>
|
||||
<Textarea
|
||||
placeholder="e.g., A fitness tracking app with workout plans, nutrition tracking, and social features for sharing progress with friends"
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Describe what kind of app you want, target users, key features, and any specific design preferences.
|
||||
</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">Configure API key in Settings</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" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
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">Clear</span>
|
||||
<span className="sm:hidden">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!generatedPrompt && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
<span className="hidden sm:inline">Best Ever UX Prompt</span>
|
||||
<span className="sm:hidden">UX Prompt</span>
|
||||
</span>
|
||||
{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">
|
||||
Comprehensive UX design prompt ready for designers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{generatedPrompt ? (
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{generatedPrompt}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4">
|
||||
Your comprehensive UX designer prompt will appear here
|
||||
</div>
|
||||
)}
|
||||
</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 }
|
||||
@@ -2,4 +2,6 @@ import ModelAdapter from "./model-adapter";
|
||||
|
||||
const adapter = new ModelAdapter();
|
||||
|
||||
adapter["qwenService"]["initialize"]?.();
|
||||
|
||||
export default adapter;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
|
||||
import QwenOAuthService from "./qwen-oauth";
|
||||
import OllamaCloudService from "./ollama-cloud";
|
||||
import ZaiPlanService from "./zai-plan";
|
||||
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
|
||||
|
||||
export interface ModelAdapterConfig {
|
||||
qwen?: {
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
};
|
||||
qwen?: QwenOAuthConfig;
|
||||
ollama?: {
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
@@ -20,34 +17,35 @@ export interface ModelAdapterConfig {
|
||||
}
|
||||
|
||||
export class ModelAdapter {
|
||||
private qwenService: QwenOAuthService;
|
||||
private ollamaService: OllamaCloudService;
|
||||
private zaiService: ZaiPlanService;
|
||||
private qwenService = qwenOAuthService;
|
||||
private preferredProvider: ModelProvider;
|
||||
|
||||
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "qwen") {
|
||||
this.qwenService = new QwenOAuthService(config.qwen);
|
||||
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "ollama") {
|
||||
this.ollamaService = new OllamaCloudService(config.ollama);
|
||||
this.zaiService = new ZaiPlanService(config.zai);
|
||||
this.preferredProvider = preferredProvider;
|
||||
|
||||
if (config.qwen) {
|
||||
if (config.qwen.apiKey) {
|
||||
this.qwenService.setApiKey(config.qwen.apiKey);
|
||||
}
|
||||
if (config.qwen.accessToken) {
|
||||
this.qwenService.setOAuthTokens({
|
||||
accessToken: config.qwen.accessToken,
|
||||
refreshToken: config.qwen.refreshToken,
|
||||
expiresAt: config.qwen.expiresAt,
|
||||
resourceUrl: config.qwen.resourceUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPreferredProvider(provider: ModelProvider): void {
|
||||
this.preferredProvider = provider;
|
||||
}
|
||||
|
||||
updateQwenApiKey(apiKey: string): void {
|
||||
this.qwenService = new QwenOAuthService({ apiKey });
|
||||
}
|
||||
|
||||
setQwenOAuthTokens(accessToken: string, refreshToken?: string, expiresIn?: number): void {
|
||||
this.qwenService.setOAuthTokens(accessToken, refreshToken, expiresIn);
|
||||
}
|
||||
|
||||
getQwenAuthUrl(): string {
|
||||
return this.qwenService.getAuthorizationUrl();
|
||||
}
|
||||
|
||||
updateOllamaApiKey(apiKey: string): void {
|
||||
this.ollamaService = new OllamaCloudService({ apiKey });
|
||||
}
|
||||
@@ -56,17 +54,77 @@ export class ModelAdapter {
|
||||
this.zaiService = new ZaiPlanService({ apiKey });
|
||||
}
|
||||
|
||||
updateQwenApiKey(apiKey: string): void {
|
||||
this.qwenService.setApiKey(apiKey);
|
||||
}
|
||||
|
||||
updateQwenTokens(tokens?: QwenOAuthToken): void {
|
||||
this.qwenService.setOAuthTokens(tokens);
|
||||
}
|
||||
|
||||
async startQwenOAuth(): Promise<QwenOAuthToken> {
|
||||
return await this.qwenService.signIn();
|
||||
}
|
||||
|
||||
getQwenTokenInfo(): QwenOAuthToken | null {
|
||||
return this.qwenService.getTokenInfo();
|
||||
}
|
||||
|
||||
hasQwenAuth(): boolean {
|
||||
return this.qwenService.hasOAuthToken();
|
||||
}
|
||||
|
||||
private isProviderAuthenticated(provider: ModelProvider): boolean {
|
||||
switch (provider) {
|
||||
case "qwen":
|
||||
return this.hasQwenAuth() || this.qwenService.hasApiKey();
|
||||
case "ollama":
|
||||
return this.ollamaService.hasAuth();
|
||||
case "zai":
|
||||
return this.zaiService.hasAuth();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private buildFallbackProviders(...providers: ModelProvider[]): ModelProvider[] {
|
||||
const seen = new Set<ModelProvider>();
|
||||
return providers.filter((provider) => {
|
||||
if (seen.has(provider)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(provider);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private async callWithFallback<T>(
|
||||
operation: (service: any) => Promise<APIResponse<T>>,
|
||||
providers: ModelProvider[]
|
||||
): Promise<APIResponse<T>> {
|
||||
console.log("[ModelAdapter] Attempting providers in order:", providers);
|
||||
let lastError: string | null = null;
|
||||
|
||||
for (const provider of providers) {
|
||||
try {
|
||||
console.log(`[ModelAdapter] Checking authentication for ${provider}...`);
|
||||
|
||||
if (!this.isProviderAuthenticated(provider)) {
|
||||
console.log(`[ModelAdapter] Provider ${provider} is not authenticated, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let service: any;
|
||||
|
||||
console.log(`[ModelAdapter] Trying provider: ${provider}`);
|
||||
|
||||
switch (provider) {
|
||||
case "qwen":
|
||||
service = this.qwenService;
|
||||
console.log("[ModelAdapter] Qwen service:", {
|
||||
hasApiKey: !!this.qwenService["apiKey"],
|
||||
hasToken: !!this.qwenService.getTokenInfo()?.accessToken
|
||||
});
|
||||
break;
|
||||
case "ollama":
|
||||
service = this.ollamaService;
|
||||
@@ -77,35 +135,97 @@ export class ModelAdapter {
|
||||
}
|
||||
|
||||
const result = await operation(service);
|
||||
console.log(`[ModelAdapter] Provider ${provider} result:`, result);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[ModelAdapter] Success with provider: ${provider}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
lastError = result.error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error with ${provider}:`, error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[ModelAdapter] Error with ${provider}:`, errorMessage);
|
||||
lastError = errorMessage || lastError;
|
||||
}
|
||||
}
|
||||
|
||||
const finalError = lastError
|
||||
? `All providers failed: ${lastError}`
|
||||
: "All providers failed. Please configure API key in Settings";
|
||||
console.error(`[ModelAdapter] ${finalError}`);
|
||||
return {
|
||||
success: false,
|
||||
error: "All providers failed",
|
||||
error: finalError,
|
||||
};
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const providers: ModelProvider[] = provider ? [provider] : [this.preferredProvider, "ollama", "zai"];
|
||||
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 providers: ModelProvider[] = provider ? [provider] : ["ollama", "zai", this.preferredProvider];
|
||||
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 providers: ModelProvider[] = provider ? [provider] : ["zai", "ollama", this.preferredProvider];
|
||||
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;
|
||||
} = { 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 chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string,
|
||||
@@ -137,7 +257,7 @@ export class ModelAdapter {
|
||||
|
||||
async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> {
|
||||
const fallbackModels: Record<ModelProvider, string[]> = {
|
||||
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"],
|
||||
qwen: this.qwenService.getAvailableModels(),
|
||||
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
|
||||
zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||
};
|
||||
@@ -163,16 +283,6 @@ export class ModelAdapter {
|
||||
console.error("[ModelAdapter] Failed to load Z.AI models, using fallback:", error);
|
||||
}
|
||||
}
|
||||
if (provider === "qwen" || !provider) {
|
||||
try {
|
||||
const qwenModels = await this.qwenService.listModels();
|
||||
if (qwenModels.success && qwenModels.data && qwenModels.data.length > 0) {
|
||||
models.qwen = qwenModels.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[ModelAdapter] Failed to load Qwen models, using fallback:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: models };
|
||||
}
|
||||
|
||||
@@ -5,11 +5,43 @@ export interface OllamaCloudConfig {
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export interface OllamaModel {
|
||||
name: string;
|
||||
size?: number;
|
||||
digest?: string;
|
||||
}
|
||||
const BASE_PATH = "/tools/promptarch";
|
||||
const LOCAL_MODELS_URL = `${BASE_PATH}/api/ollama/models`;
|
||||
const LOCAL_CHAT_URL = `${BASE_PATH}/api/ollama/chat`;
|
||||
const DEFAULT_MODELS = [
|
||||
"gpt-oss:120b",
|
||||
"llama3.1:latest",
|
||||
"llama3.1:70b",
|
||||
"llama3.1:8b",
|
||||
"llama3.1:instruct",
|
||||
"gemma3:12b",
|
||||
"gemma3:27b",
|
||||
"gemma3:4b",
|
||||
"gemma3:7b",
|
||||
"deepseek-r1:70b",
|
||||
"deepseek-r1:32b",
|
||||
"deepseek-r1:14b",
|
||||
"deepseek-r1:8b",
|
||||
"deepseek-r1:1.5b",
|
||||
"qwen3:72b",
|
||||
"qwen3:32b",
|
||||
"qwen3:14b",
|
||||
"qwen3:7b",
|
||||
"qwen3:4b",
|
||||
"mistral:7b",
|
||||
"mistral:instruct",
|
||||
"codellama:34b",
|
||||
"codellama:13b",
|
||||
"codellama:7b",
|
||||
"codellama:instruct",
|
||||
"phi3:14b",
|
||||
"phi3:3.8b",
|
||||
"phi3:mini",
|
||||
"gemma2:27b",
|
||||
"gemma2:9b",
|
||||
"yi:34b",
|
||||
"yi:9b",
|
||||
];
|
||||
|
||||
export class OllamaCloudService {
|
||||
private config: OllamaCloudConfig;
|
||||
@@ -17,38 +49,50 @@ export class OllamaCloudService {
|
||||
|
||||
constructor(config: OllamaCloudConfig = {}) {
|
||||
this.config = {
|
||||
endpoint: config.endpoint || "https://ollama.com/api",
|
||||
apiKey: config.apiKey || process.env.OLLAMA_API_KEY,
|
||||
endpoint: config.endpoint,
|
||||
};
|
||||
}
|
||||
|
||||
private getHeaders(): Record<string, string> {
|
||||
hasAuth(): boolean {
|
||||
return !!this.config.apiKey;
|
||||
}
|
||||
|
||||
private ensureApiKey(): string {
|
||||
if (this.config.apiKey) {
|
||||
return this.config.apiKey;
|
||||
}
|
||||
throw new Error("API key is required. Please configure your Ollama API key in settings.");
|
||||
}
|
||||
|
||||
private getHeaders(additional: Record<string, string> = {}) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...additional,
|
||||
"x-ollama-api-key": this.ensureApiKey(),
|
||||
};
|
||||
|
||||
if (this.config.apiKey) {
|
||||
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
||||
if (this.config.endpoint) {
|
||||
headers["x-ollama-endpoint"] = this.config.endpoint;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private async parseJsonResponse(response: Response): Promise<any> {
|
||||
const text = await response.text();
|
||||
if (!text) return null;
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string = "gpt-oss:120b",
|
||||
stream: boolean = false
|
||||
): Promise<APIResponse<string>> {
|
||||
try {
|
||||
if (!this.config.apiKey) {
|
||||
throw new Error("API key is required. Please configure your Ollama API key in settings.");
|
||||
}
|
||||
|
||||
console.log("[Ollama] API call:", { endpoint: this.config.endpoint, model, messages });
|
||||
|
||||
const response = await fetch(`${this.config.endpoint}/chat`, {
|
||||
const response = await fetch(LOCAL_CHAT_URL, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
headers: this.getHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages,
|
||||
@@ -56,24 +100,23 @@ export class OllamaCloudService {
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("[Ollama] Response status:", response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[Ollama] Error response:", errorText);
|
||||
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
|
||||
const errorBody = await response.text();
|
||||
throw new Error(
|
||||
`Chat completion failed (${response.status}): ${response.statusText} - ${errorBody}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Ollama] Response data:", data);
|
||||
|
||||
if (data.message && data.message.content) {
|
||||
const data = await this.parseJsonResponse(response);
|
||||
if (data?.message?.content) {
|
||||
return { success: true, data: data.message.content };
|
||||
} else if (data.choices && data.choices[0]) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
} else {
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
}
|
||||
|
||||
if (data?.choices?.[0]?.message?.content) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
}
|
||||
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
} catch (error) {
|
||||
console.error("[Ollama] Chat completion error:", error);
|
||||
return {
|
||||
@@ -85,32 +128,31 @@ export class OllamaCloudService {
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
try {
|
||||
if (this.config.apiKey) {
|
||||
console.log("[Ollama] Listing models from:", `${this.config.endpoint}/tags`);
|
||||
const response = await fetch(LOCAL_MODELS_URL, {
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.config.endpoint}/tags`, {
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
console.log("[Ollama] List models response status:", response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list models: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const 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"] };
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(`List models failed: ${response.statusText} - ${errorBody}`);
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error("[Ollama] listModels error:", error);
|
||||
if (DEFAULT_MODELS.length > 0) {
|
||||
this.availableModels = DEFAULT_MODELS;
|
||||
return { success: true, data: DEFAULT_MODELS };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to list models",
|
||||
@@ -119,9 +161,7 @@ export class OllamaCloudService {
|
||||
}
|
||||
|
||||
getAvailableModels(): string[] {
|
||||
return this.availableModels.length > 0
|
||||
? this.availableModels
|
||||
: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"];
|
||||
return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS;
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
|
||||
@@ -136,7 +176,7 @@ Apply these principles:
|
||||
4. Include edge cases and error handling requirements
|
||||
5. Specify testing and validation criteria
|
||||
|
||||
Return ONLY the enhanced prompt, no explanations.`,
|
||||
Return ONLY the enhanced prompt, no explanations or extra text.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
@@ -198,6 +238,296 @@ Include specific recommendations for:
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a world-class UX/UI designer with deep expertise in human-centered design principles, user research, interaction design, visual design systems, and modern design tools (Figma, Sketch, Adobe XD).
|
||||
|
||||
Your task is to create an exceptional, detailed prompt for generating best possible UX design for a given app description.
|
||||
|
||||
Generate a comprehensive UX design prompt that includes:
|
||||
|
||||
1. USER RESEARCH & PERSONAS
|
||||
- Primary target users and their motivations
|
||||
- User pain points and needs
|
||||
- User journey maps
|
||||
- Persona archetypes with demographics and goals
|
||||
|
||||
2. INFORMATION ARCHITECTURE
|
||||
- Content hierarchy and organization
|
||||
- Navigation structure and patterns
|
||||
- User flows and key pathways
|
||||
- Site map or app structure
|
||||
|
||||
3. VISUAL DESIGN SYSTEM
|
||||
- Color palette recommendations (primary, secondary, accent, neutral)
|
||||
- Typography hierarchy and font pairings
|
||||
- Component library approach
|
||||
- Spacing, sizing, and layout grids
|
||||
- Iconography style and set
|
||||
|
||||
4. INTERACTION DESIGN
|
||||
- Micro-interactions and animations
|
||||
- Gesture patterns for touch interfaces
|
||||
- Loading states and empty states
|
||||
- Error handling and feedback mechanisms
|
||||
- Accessibility considerations (WCAG compliance)
|
||||
|
||||
5. KEY SCREENS & COMPONENTS
|
||||
- Core screens that need detailed design
|
||||
- Critical components (buttons, forms, cards, navigation)
|
||||
- Data visualization needs
|
||||
- Responsive design requirements (mobile, tablet, desktop)
|
||||
|
||||
6. DESIGN DELIVERABLES
|
||||
- Wireframes vs. high-fidelity mockups
|
||||
- Design system documentation needs
|
||||
- Prototyping requirements
|
||||
- Handoff specifications for developers
|
||||
|
||||
7. COMPETITIVE INSIGHTS
|
||||
- Design patterns from successful apps in this category
|
||||
- Opportunities to differentiate
|
||||
- Modern design trends to consider
|
||||
|
||||
The output should be a detailed, actionable prompt that a designer or AI image generator can use to create world-class UX designs.
|
||||
|
||||
Make's prompt specific, inspiring, and comprehensive. Use professional UX terminology.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a BEST EVER UX design prompt for this app:\n\n${appDescription}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
animationStyle?: string;
|
||||
audienceStyle?: string;
|
||||
themeColors?: string[];
|
||||
brandColors?: string[];
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
language = "English",
|
||||
theme = "executive-dark",
|
||||
slideCount = 10,
|
||||
audience = "Executives & C-Suite",
|
||||
organization = "",
|
||||
animationStyle = "Professional",
|
||||
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||
brandColors = []
|
||||
} = options;
|
||||
|
||||
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||
const brandColorStr = brandColors.length > 0
|
||||
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||
: "";
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||
|
||||
Your slides must be VISUALLY SPECTACULAR with:
|
||||
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||
- Sophisticated gradient backgrounds with depth
|
||||
- SVG charts and data visualizations inline
|
||||
- Glassmorphism and neumorphism effects
|
||||
- Professional typography with Inter/SF Pro fonts
|
||||
- Strategic use of whitespace
|
||||
- Micro-animations on hover/focus states
|
||||
- Progress indicators and visual hierarchy
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Compelling Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Plain text content summary",
|
||||
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN SYSTEM:
|
||||
- Primary: ${brandColors[0] || primaryColor}
|
||||
- Secondary: ${brandColors[1] || secondaryColor}
|
||||
- Background: ${bgColor}
|
||||
- Text: ${textColor}${brandColorStr}
|
||||
|
||||
ANIMATION STYLE: ${animationStyle}
|
||||
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||
|
||||
CSS ANIMATIONS TO INCLUDE:
|
||||
\`\`\`css
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||
\`\`\`
|
||||
|
||||
SLIDE TYPES TO CREATE:
|
||||
1. TITLE SLIDE: Hero-style with animated gradient background, large typography
|
||||
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||
4. KEY METRICS: Large animated numbers with KPI cards
|
||||
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||
7. QUOTE: Large typography with decorative quote marks
|
||||
8. CALL-TO-ACTION: Bold CTA with pulsing button effect
|
||||
|
||||
TARGET AUDIENCE: ${audience}
|
||||
AUDIENCE STYLE: ${audienceStyle}
|
||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||
|
||||
REQUIREMENTS:
|
||||
- Create EXACTLY ${slideCount} slides
|
||||
- ALL content in ${language}
|
||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||
- Use animation-delay for staggered reveal effects
|
||||
- Include decorative background elements (gradients, shapes)
|
||||
- Ensure text contrast meets WCAG AA standards
|
||||
- Add subtle shadow/glow effects for depth`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a STUNNING, ANIMATED presentation about:
|
||||
|
||||
${topic}
|
||||
|
||||
SPECIFICATIONS:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Slides: ${slideCount}
|
||||
- Audience: ${audience} (${audienceStyle})
|
||||
- Animation Style: ${animationStyle}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||
|
||||
Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients, and corporate-ready design!`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English"
|
||||
} = 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(", ")}` : ""}
|
||||
|
||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
}
|
||||
|
||||
export default OllamaCloudService;
|
||||
|
||||
|
||||
|
||||
@@ -1,125 +1,487 @@
|
||||
import type { ChatMessage, APIResponse } from "@/types";
|
||||
|
||||
const DEFAULT_QWEN_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
const TOKEN_STORAGE_KEY = "promptarch-qwen-tokens";
|
||||
|
||||
function getOAuthBaseUrl(): string {
|
||||
const basePath = '/tools/promptarch';
|
||||
if (typeof window !== "undefined") {
|
||||
const origin = window.location.origin;
|
||||
return `${origin}${basePath}/api/qwen`;
|
||||
}
|
||||
if (process.env.NEXT_PUBLIC_SITE_URL) {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL.replace(/\/$/, "");
|
||||
if (siteUrl.endsWith(basePath)) {
|
||||
return `${siteUrl}/api/qwen`;
|
||||
}
|
||||
return `${siteUrl}${basePath}/api/qwen`;
|
||||
}
|
||||
return `${basePath}/api/qwen`;
|
||||
}
|
||||
|
||||
export interface QwenOAuthConfig {
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
oauthBaseUrl?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
endpoint?: string;
|
||||
clientId?: string;
|
||||
redirectUri?: string;
|
||||
resourceUrl?: string;
|
||||
}
|
||||
|
||||
export interface QwenOAuthToken {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
resourceUrl?: string;
|
||||
}
|
||||
|
||||
export interface QwenDeviceAuthorization {
|
||||
device_code: string;
|
||||
user_code: string;
|
||||
verification_uri: string;
|
||||
verification_uri_complete: string;
|
||||
expires_in: number;
|
||||
interval?: number;
|
||||
}
|
||||
|
||||
export class QwenOAuthService {
|
||||
private config: QwenOAuthConfig;
|
||||
private endpoint: string;
|
||||
private oauthBaseUrl: string;
|
||||
private apiKey?: string;
|
||||
private token: QwenOAuthToken | null = null;
|
||||
private storageHydrated = false;
|
||||
|
||||
constructor(config: QwenOAuthConfig = {}) {
|
||||
this.config = {
|
||||
endpoint: config.endpoint || "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: config.apiKey || process.env.QWEN_API_KEY,
|
||||
accessToken: config.accessToken,
|
||||
refreshToken: config.refreshToken,
|
||||
expiresAt: config.expiresAt,
|
||||
clientId: config.clientId || process.env.NEXT_PUBLIC_QWEN_CLIENT_ID,
|
||||
redirectUri: config.redirectUri || (typeof window !== "undefined" ? window.location.origin : ""),
|
||||
};
|
||||
}
|
||||
this.endpoint = config.endpoint || DEFAULT_QWEN_ENDPOINT;
|
||||
this.oauthBaseUrl = config.oauthBaseUrl || getOAuthBaseUrl();
|
||||
this.apiKey = config.apiKey || process.env.QWEN_API_KEY || undefined;
|
||||
|
||||
private getHeaders(): Record<string, string> {
|
||||
const authHeader = this.config.accessToken
|
||||
? `Bearer ${this.config.accessToken}`
|
||||
: `Bearer ${this.config.apiKey}`;
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": authHeader,
|
||||
};
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!(this.config.apiKey || (this.config.accessToken && (!this.config.expiresAt || this.config.expiresAt > Date.now())));
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
return this.config.accessToken || this.config.apiKey || null;
|
||||
}
|
||||
|
||||
async authenticate(apiKey: string): Promise<APIResponse<string>> {
|
||||
try {
|
||||
this.config.apiKey = apiKey;
|
||||
this.config.accessToken = undefined; // Clear OAuth token if API key is provided
|
||||
return { success: true, data: "Authenticated successfully" };
|
||||
} catch (error) {
|
||||
console.error("Qwen authentication error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Authentication failed",
|
||||
};
|
||||
if (config.accessToken) {
|
||||
this.setOAuthTokens({
|
||||
accessToken: config.accessToken,
|
||||
refreshToken: config.refreshToken,
|
||||
expiresAt: config.expiresAt,
|
||||
resourceUrl: config.resourceUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setOAuthTokens(accessToken: string, refreshToken?: string, expiresIn?: number): void {
|
||||
this.config.accessToken = accessToken;
|
||||
if (refreshToken) this.config.refreshToken = refreshToken;
|
||||
if (expiresIn) this.config.expiresAt = Date.now() + expiresIn * 1000;
|
||||
/**
|
||||
* Update the API key used for non-OAuth calls.
|
||||
*/
|
||||
setApiKey(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
getAuthorizationUrl(): string {
|
||||
const baseUrl = "https://dashscope.console.aliyun.com/oauth/authorize"; // Placeholder URL
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.config.clientId || "",
|
||||
redirect_uri: this.config.redirectUri || "",
|
||||
response_type: "code",
|
||||
scope: "dashscope:chat",
|
||||
hasApiKey(): boolean {
|
||||
return !!this.apiKey;
|
||||
}
|
||||
|
||||
hasOAuthToken(): boolean {
|
||||
return !!this.getTokenInfo()?.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build default headers for Qwen completions (includes OAuth token refresh).
|
||||
*/
|
||||
private async getRequestHeaders(): Promise<Record<string, string>> {
|
||||
console.log("[QwenOAuth] Getting request headers...");
|
||||
|
||||
const token = await this.getValidToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (token?.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${token.accessToken}`;
|
||||
console.log("[QwenOAuth] Using OAuth token for authorization");
|
||||
return headers;
|
||||
}
|
||||
|
||||
if (this.apiKey) {
|
||||
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
||||
console.log("[QwenOAuth] Using API key for authorization");
|
||||
return headers;
|
||||
}
|
||||
|
||||
console.error("[QwenOAuth] No OAuth token or API key available");
|
||||
throw new Error("Please configure a Qwen API key or authenticate via OAuth.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the effective API endpoint (uses token-specific resource_url if available).
|
||||
*/
|
||||
private getEffectiveEndpoint(): string {
|
||||
const resourceUrl = this.token?.resourceUrl;
|
||||
if (resourceUrl) {
|
||||
const normalized = this.normalizeResourceUrl(resourceUrl);
|
||||
console.log("[Qwen] Using resource URL:", normalized);
|
||||
return normalized;
|
||||
}
|
||||
console.log("[Qwen] Using default endpoint:", this.endpoint);
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
private normalizeResourceUrl(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
const withProtocol = trimmed.startsWith("http") ? trimmed : `https://${trimmed}`;
|
||||
const cleaned = withProtocol.replace(/\/$/, "");
|
||||
|
||||
if (cleaned.endsWith("/v1") || cleaned.endsWith("/compatible-mode/v1")) {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
return `${cleaned}/v1`;
|
||||
}
|
||||
|
||||
private hydrateTokens() {
|
||||
if (this.storageHydrated || typeof window === "undefined" || typeof window.localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
if (stored) {
|
||||
this.token = JSON.parse(stored);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[QwenOAuth] Failed to read tokens from localStorage:", error);
|
||||
this.token = null;
|
||||
} finally {
|
||||
this.storageHydrated = true;
|
||||
}
|
||||
}
|
||||
|
||||
private getStoredToken(): QwenOAuthToken | null {
|
||||
this.hydrateTokens();
|
||||
console.log("[QwenOAuth] Retrieved stored token:", this.token ? { hasAccessToken: !!this.token.accessToken, expiresAt: this.token.expiresAt } : null);
|
||||
return this.token;
|
||||
}
|
||||
|
||||
private persistToken(token: QwenOAuthToken | null) {
|
||||
if (typeof window === "undefined" || typeof window.localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (token) {
|
||||
window.localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
||||
} else {
|
||||
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[QwenOAuth] Failed to persist tokens to localStorage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private isTokenExpired(token: QwenOAuthToken): boolean {
|
||||
if (!token.expiresAt) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() >= token.expiresAt - 60_000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the OAuth token using the stored refresh token.
|
||||
*/
|
||||
private async refreshToken(refreshToken: string): Promise<QwenOAuthToken> {
|
||||
const response = await fetch(`${this.oauthBaseUrl}/oauth/refresh`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || "Failed to refresh Qwen token");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return this.parseTokenResponse(data);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.config.apiKey = undefined;
|
||||
this.config.accessToken = undefined;
|
||||
this.config.refreshToken = undefined;
|
||||
this.config.expiresAt = undefined;
|
||||
/**
|
||||
* Returns a valid token, refreshing if necessary.
|
||||
*/
|
||||
private async getValidToken(): Promise<QwenOAuthToken | null> {
|
||||
const token = this.getStoredToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.isTokenExpired(token)) {
|
||||
if (token.refreshToken) {
|
||||
try {
|
||||
const refreshed = await this.refreshToken(token.refreshToken);
|
||||
this.setOAuthTokens(refreshed);
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
console.error("Qwen token refresh failed", error);
|
||||
this.setOAuthTokens(undefined);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
this.setOAuthTokens(undefined);
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out the OAuth session.
|
||||
*/
|
||||
signOut(): void {
|
||||
this.setOAuthTokens(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores OAuth tokens locally.
|
||||
*/
|
||||
setOAuthTokens(tokens?: QwenOAuthToken | null) {
|
||||
if (!tokens) {
|
||||
this.token = null;
|
||||
this.persistToken(null);
|
||||
this.storageHydrated = true;
|
||||
return;
|
||||
}
|
||||
this.token = tokens;
|
||||
this.persistToken(tokens);
|
||||
this.storageHydrated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the service and hydrate tokens from storage.
|
||||
*/
|
||||
initialize(): void {
|
||||
console.log("[QwenOAuth] Initializing service...");
|
||||
this.hydrateTokens();
|
||||
}
|
||||
|
||||
getTokenInfo(): QwenOAuthToken | null {
|
||||
this.hydrateTokens();
|
||||
console.log("[QwenOAuth] getTokenInfo called, returning:", this.token ? { hasAccessToken: !!this.token.accessToken, expiresAt: this.token.expiresAt } : null);
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the OAuth device flow to obtain tokens.
|
||||
*/
|
||||
async signIn(): Promise<QwenOAuthToken> {
|
||||
if (typeof window === "undefined") {
|
||||
throw new Error("Qwen OAuth is only supported in the browser");
|
||||
}
|
||||
|
||||
const popup = window.open(
|
||||
"",
|
||||
"qwen-oauth",
|
||||
"width=500,height=600,scrollbars=yes,resizable=yes"
|
||||
);
|
||||
|
||||
const codeVerifier = this.generateCodeVerifier();
|
||||
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
|
||||
const deviceAuth = await this.requestDeviceAuthorization(codeChallenge);
|
||||
|
||||
if (popup) {
|
||||
try {
|
||||
popup.location.href = deviceAuth.verification_uri_complete;
|
||||
} catch {
|
||||
// ignore cross-origin restrictions
|
||||
}
|
||||
} else {
|
||||
window.alert(
|
||||
`Open this URL to authenticate:\n${deviceAuth.verification_uri_complete}\n\nUser code: ${deviceAuth.user_code}`
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + deviceAuth.expires_in * 1000;
|
||||
let pollInterval = 2000;
|
||||
|
||||
while (Date.now() < expiresAt) {
|
||||
const tokenData = await this.pollDeviceToken(deviceAuth.device_code, codeVerifier);
|
||||
|
||||
if (tokenData?.access_token) {
|
||||
const token = this.parseTokenResponse(tokenData);
|
||||
this.setOAuthTokens(token);
|
||||
popup?.close();
|
||||
return token;
|
||||
}
|
||||
|
||||
if (tokenData?.error === "authorization_pending") {
|
||||
await this.delay(pollInterval);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tokenData?.error === "slow_down") {
|
||||
pollInterval = Math.min(Math.ceil(pollInterval * 1.5), 10000);
|
||||
await this.delay(pollInterval);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(tokenData?.error_description || tokenData?.error || "OAuth failed");
|
||||
}
|
||||
|
||||
throw new Error("Qwen OAuth timed out");
|
||||
}
|
||||
|
||||
async fetchUserInfo(): Promise<unknown> {
|
||||
const token = await this.getValidToken();
|
||||
if (!token?.accessToken) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.oauthBaseUrl}/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || "Failed to fetch user info");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async requestDeviceAuthorization(codeChallenge: string): Promise<QwenDeviceAuthorization> {
|
||||
const response = await fetch(`${this.oauthBaseUrl}/oauth/device`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || "Device authorization failed");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async pollDeviceToken(deviceCode: string, codeVerifier: string): Promise<any> {
|
||||
const response = await fetch(`${this.oauthBaseUrl}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_code: deviceCode,
|
||||
code_verifier: codeVerifier,
|
||||
}),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private parseTokenResponse(data: any): QwenOAuthToken {
|
||||
console.log("[QwenOAuth] Token response received:", data);
|
||||
|
||||
const token: QwenOAuthToken = {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined,
|
||||
};
|
||||
|
||||
if (data.resource_url) {
|
||||
token.resourceUrl = data.resource_url;
|
||||
console.log("[QwenOAuth] Using resource_url from response:", data.resource_url);
|
||||
} else if (data.endpoint) {
|
||||
token.resourceUrl = data.endpoint;
|
||||
console.log("[QwenOAuth] Using endpoint from response:", data.endpoint);
|
||||
} else if (data.resource_server) {
|
||||
token.resourceUrl = `https://${data.resource_server}/compatible-mode/v1`;
|
||||
console.log("[QwenOAuth] Using resource_server from response:", data.resource_server);
|
||||
} else {
|
||||
console.log("[QwenOAuth] No resource_url/endpoint in response, will use default Qwen endpoint");
|
||||
console.log("[QwenOAuth] Available fields in response:", Object.keys(data));
|
||||
}
|
||||
|
||||
console.log("[QwenOAuth] Parsed token:", { hasAccessToken: !!token.accessToken, hasRefreshToken: !!token.refreshToken, hasResourceUrl: !!token.resourceUrl, expiresAt: token.expiresAt });
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a PKCE code verifier.
|
||||
*/
|
||||
private generateCodeVerifier(): string {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return this.toBase64Url(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a PKCE code challenge.
|
||||
*/
|
||||
private async generateCodeChallenge(verifier: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(verifier);
|
||||
const digest = await crypto.subtle.digest("SHA-256", data);
|
||||
return this.toBase64Url(new Uint8Array(digest));
|
||||
}
|
||||
|
||||
private toBase64Url(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string = "qwen-coder-plus",
|
||||
model: string = "coder-model",
|
||||
stream: boolean = false
|
||||
): Promise<APIResponse<string>> {
|
||||
try {
|
||||
if (!this.config.apiKey) {
|
||||
throw new Error("API key is required. Please configure your Qwen API key in settings.");
|
||||
}
|
||||
const headers = await this.getRequestHeaders();
|
||||
const baseUrl = this.getEffectiveEndpoint();
|
||||
const url = `${this.oauthBaseUrl}/chat`;
|
||||
|
||||
console.log("[Qwen] API call:", { endpoint: this.config.endpoint, model, messages });
|
||||
console.log("[Qwen] Chat completion request:", { url, model, hasAuth: !!headers.Authorization });
|
||||
|
||||
const response = await fetch(`${this.config.endpoint}/chat/completions`, {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: headers.Authorization || "",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
endpoint: baseUrl,
|
||||
model,
|
||||
messages,
|
||||
stream,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("[Qwen] Response status:", response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[Qwen] Error response:", errorText);
|
||||
console.error("[Qwen] Chat completion failed:", response.status, response.statusText, errorText);
|
||||
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Qwen] Response data:", data);
|
||||
|
||||
if (data.choices && data.choices[0] && data.choices[0].message) {
|
||||
if (data.choices?.[0]?.message) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
} else {
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
}
|
||||
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
} catch (error) {
|
||||
console.error("[Qwen] Chat completion error:", error);
|
||||
return {
|
||||
@@ -149,7 +511,7 @@ Return ONLY the enhanced prompt, no explanations or extra text.`,
|
||||
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus");
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> {
|
||||
@@ -173,7 +535,7 @@ Use clear, specific language suitable for development teams.`,
|
||||
content: `Generate a PRD for this idea:\n\n${idea}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus");
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> {
|
||||
@@ -201,17 +563,313 @@ Include specific recommendations for:
|
||||
content: `Generate an action plan based on this PRD:\n\n${prd}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus");
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
|
||||
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 || "coder-model");
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
animationStyle?: string;
|
||||
audienceStyle?: string;
|
||||
themeColors?: string[];
|
||||
brandColors?: string[];
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
language = "English",
|
||||
theme = "executive-dark",
|
||||
slideCount = 10,
|
||||
audience = "Executives & C-Suite",
|
||||
organization = "",
|
||||
animationStyle = "Professional",
|
||||
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||
brandColors = []
|
||||
} = options;
|
||||
|
||||
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||
const brandColorStr = brandColors.length > 0
|
||||
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||
: "";
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||
|
||||
Your slides must be VISUALLY SPECTACULAR with:
|
||||
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||
- Sophisticated gradient backgrounds with depth
|
||||
- SVG charts and data visualizations inline
|
||||
- Glassmorphism and neumorphism effects
|
||||
- Professional typography with Inter/SF Pro fonts
|
||||
- Strategic use of whitespace
|
||||
- Micro-animations on hover/focus states
|
||||
- Progress indicators and visual hierarchy
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Compelling Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Plain text content summary",
|
||||
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN SYSTEM:
|
||||
- Primary: ${brandColors[0] || primaryColor}
|
||||
- Secondary: ${brandColors[1] || secondaryColor}
|
||||
- Background: ${bgColor}
|
||||
- Text: ${textColor}${brandColorStr}
|
||||
|
||||
ANIMATION STYLE: ${animationStyle}
|
||||
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||
|
||||
CSS ANIMATIONS TO INCLUDE:
|
||||
\`\`\`css
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||
\`\`\`
|
||||
|
||||
SLIDE TYPES TO CREATE:
|
||||
1. TITLE SLIDE: Hero-style with animated gradient background, large typography
|
||||
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||
4. KEY METRICS: Large animated numbers with KPI cards
|
||||
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||
7. QUOTE: Large typography with decorative quote marks
|
||||
8. CALL-TO-ACTION: Bold CTA with pulsing button effect
|
||||
|
||||
TARGET AUDIENCE: ${audience}
|
||||
AUDIENCE STYLE: ${audienceStyle}
|
||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||
|
||||
REQUIREMENTS:
|
||||
- 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 || "coder-model");
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English"
|
||||
} = 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(", ")}` : ""}
|
||||
|
||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
const models = ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite", "qwen-plus", "qwen-turbo", "qwen-max"];
|
||||
const models = [
|
||||
"coder-model",
|
||||
];
|
||||
return { success: true, data: models };
|
||||
}
|
||||
|
||||
getAvailableModels(): string[] {
|
||||
return ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite", "qwen-plus", "qwen-turbo", "qwen-max"];
|
||||
return [
|
||||
"coder-model",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export default QwenOAuthService;
|
||||
const qwenOAuthService = new QwenOAuthService();
|
||||
export default qwenOAuthService;
|
||||
export { qwenOAuthService };
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ export class ZaiPlanService {
|
||||
};
|
||||
}
|
||||
|
||||
hasAuth(): boolean {
|
||||
return !!this.config.apiKey;
|
||||
}
|
||||
|
||||
private getHeaders(): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
@@ -182,6 +186,427 @@ Include specific recommendations for:
|
||||
getAvailableModels(): string[] {
|
||||
return ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"];
|
||||
}
|
||||
|
||||
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a world-class UX/UI designer with deep expertise in human-centered design principles, user research, interaction design, visual design systems, and modern design tools (Figma, Sketch, Adobe XD).
|
||||
|
||||
Your task is to create an exceptional, detailed prompt for generating the best possible UX design for a given app description.
|
||||
|
||||
Generate a comprehensive UX design prompt that includes:
|
||||
|
||||
1. USER RESEARCH & PERSONAS
|
||||
- Primary target users and their motivations
|
||||
- User pain points and needs
|
||||
- User journey maps
|
||||
- Persona archetypes with demographics and goals
|
||||
|
||||
2. INFORMATION ARCHITECTURE
|
||||
- Content hierarchy and organization
|
||||
- Navigation structure and patterns
|
||||
- User flows and key pathways
|
||||
- Site map or app structure
|
||||
|
||||
3. VISUAL DESIGN SYSTEM
|
||||
- Color palette recommendations (primary, secondary, accent, neutral)
|
||||
- Typography hierarchy and font pairings
|
||||
- Component library approach
|
||||
- Spacing, sizing, and layout grids
|
||||
- Iconography style and set
|
||||
|
||||
4. INTERACTION DESIGN
|
||||
- Micro-interactions and animations
|
||||
- Gesture patterns for touch interfaces
|
||||
- Loading states and empty states
|
||||
- Error handling and feedback mechanisms
|
||||
- Accessibility considerations (WCAG compliance)
|
||||
|
||||
5. KEY SCREENS & COMPONENTS
|
||||
- Core screens that need detailed design
|
||||
- Critical components (buttons, forms, cards, navigation)
|
||||
- Data visualization needs
|
||||
- Responsive design requirements (mobile, tablet, desktop)
|
||||
|
||||
6. DESIGN DELIVERABLES
|
||||
- Wireframes vs. high-fidelity mockups
|
||||
- Design system documentation needs
|
||||
- Prototyping requirements
|
||||
- Handoff specifications for developers
|
||||
|
||||
7. COMPETITIVE INSIGHTS
|
||||
- Design patterns from successful apps in this category
|
||||
- Opportunities to differentiate
|
||||
- Modern design trends to consider
|
||||
|
||||
The output should be a detailed, actionable prompt that a designer or AI image generator can use to create world-class UX designs.
|
||||
|
||||
Make the prompt specific, inspiring, and comprehensive. Use professional UX terminology.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create the BEST EVER UX design prompt for this app:\n\n${appDescription}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
animationStyle?: string;
|
||||
audienceStyle?: string;
|
||||
themeColors?: string[];
|
||||
brandColors?: string[];
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
language = "English",
|
||||
theme = "executive-dark",
|
||||
slideCount = 10,
|
||||
audience = "Executives & C-Suite",
|
||||
organization = "",
|
||||
animationStyle = "Professional",
|
||||
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||
brandColors = []
|
||||
} = options;
|
||||
|
||||
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||
const brandColorStr = brandColors.length > 0
|
||||
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||
: "";
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||
|
||||
Your slides must be VISUALLY SPECTACULAR with:
|
||||
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||
- Sophisticated gradient backgrounds with depth
|
||||
- SVG charts and data visualizations inline
|
||||
- Glassmorphism and neumorphism effects
|
||||
- Professional typography with Inter/SF Pro fonts
|
||||
- Strategic use of whitespace
|
||||
- Micro-animations on hover/focus states
|
||||
- Progress indicators and visual hierarchy
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Compelling Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Plain text content summary",
|
||||
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN SYSTEM:
|
||||
- Primary: ${brandColors[0] || primaryColor}
|
||||
- Secondary: ${brandColors[1] || secondaryColor}
|
||||
- Background: ${bgColor}
|
||||
- Text: ${textColor}${brandColorStr}
|
||||
|
||||
ANIMATION STYLE: ${animationStyle}
|
||||
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||
|
||||
CSS ANIMATIONS TO INCLUDE:
|
||||
\`\`\`css
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
|
||||
@keyframes gradientShift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
\`\`\`
|
||||
|
||||
SLIDE TYPES TO CREATE:
|
||||
1. TITLE SLIDE: Hero-style with animated gradient background, large typography, subtle floating elements
|
||||
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||
4. KEY METRICS: Large animated numbers with counting effect styling, KPI cards with glassmorphism
|
||||
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||
7. QUOTE: Large typography with decorative quote marks, subtle background pattern
|
||||
8. CALL-TO-ACTION: Bold CTA with pulsing button effect, clear next steps
|
||||
|
||||
SVG CHART EXAMPLE:
|
||||
\`\`\`html
|
||||
<svg viewBox="0 0 400 200" style="width:100%;max-width:400px;">
|
||||
<defs>
|
||||
<linearGradient id="barGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${primaryColor}"/>
|
||||
<stop offset="100%" style="stop-color:${secondaryColor}"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="50" y="50" width="60" height="130" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.2s both; transform-origin: bottom;"/>
|
||||
<rect x="130" y="80" width="60" height="100" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.4s both; transform-origin: bottom;"/>
|
||||
<rect x="210" y="30" width="60" height="150" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.6s both; transform-origin: bottom;"/>
|
||||
</svg>
|
||||
\`\`\`
|
||||
|
||||
TARGET AUDIENCE: ${audience}
|
||||
AUDIENCE STYLE: ${audienceStyle}
|
||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||
|
||||
REQUIREMENTS:
|
||||
- Create EXACTLY ${slideCount} slides
|
||||
- ALL content in ${language}
|
||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||
- Use animation-delay for staggered reveal effects
|
||||
- Include decorative background elements (gradients, shapes, patterns)
|
||||
- Ensure text contrast meets WCAG AA standards
|
||||
- Add subtle shadow/glow effects for depth
|
||||
- Include progress/slide number indicator styling`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a STUNNING, ANIMATED presentation about:
|
||||
|
||||
${topic}
|
||||
|
||||
SPECIFICATIONS:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Slides: ${slideCount}
|
||||
- Audience: ${audience} (${audienceStyle})
|
||||
- Animation Style: ${animationStyle}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||
|
||||
Generate SPECTACULAR slides with:
|
||||
✨ Animated CSS3 transitions and keyframes
|
||||
📊 SVG charts and data visualizations where relevant
|
||||
🎨 Modern gradients and glassmorphism effects
|
||||
💫 Staggered reveal animations
|
||||
🏢 Corporate-ready, executive-level design
|
||||
|
||||
Return the complete JSON with full htmlContent for each slide. Make each slide VISUALLY IMPRESSIVE and memorable!`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English"
|
||||
} = 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(", ")}` : ""}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default ZaiPlanService;
|
||||
|
||||
|
||||
|
||||
20
lib/store.ts
20
lib/store.ts
@@ -1,11 +1,13 @@
|
||||
import { create } from "zustand";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan } from "@/types";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult } from "@/types";
|
||||
|
||||
interface AppState {
|
||||
currentPrompt: string;
|
||||
enhancedPrompt: string | null;
|
||||
prd: PRD | null;
|
||||
actionPlan: ActionPlan | null;
|
||||
slidesPresentation: SlidesPresentation | null;
|
||||
googleAdsResult: GoogleAdsResult | null;
|
||||
selectedProvider: ModelProvider;
|
||||
selectedModels: Record<ModelProvider, string>;
|
||||
availableModels: Record<ModelProvider, string[]>;
|
||||
@@ -14,7 +16,7 @@ interface AppState {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
};
|
||||
} | null;
|
||||
isProcessing: boolean;
|
||||
error: string | null;
|
||||
history: {
|
||||
@@ -27,11 +29,13 @@ interface AppState {
|
||||
setEnhancedPrompt: (enhanced: string | null) => void;
|
||||
setPRD: (prd: PRD) => void;
|
||||
setActionPlan: (plan: ActionPlan) => void;
|
||||
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||
setSelectedProvider: (provider: ModelProvider) => void;
|
||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||
setApiKey: (provider: ModelProvider, key: string) => void;
|
||||
setQwenTokens: (tokens: { accessToken: string; refreshToken?: string; expiresAt?: number }) => void;
|
||||
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
|
||||
setProcessing: (processing: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
addToHistory: (prompt: string) => void;
|
||||
@@ -44,14 +48,16 @@ const useStore = create<AppState>((set) => ({
|
||||
enhancedPrompt: null,
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
googleAdsResult: null,
|
||||
selectedProvider: "qwen",
|
||||
selectedModels: {
|
||||
qwen: "qwen-coder-plus",
|
||||
qwen: "coder-model",
|
||||
ollama: "gpt-oss:120b",
|
||||
zai: "glm-4.7",
|
||||
},
|
||||
availableModels: {
|
||||
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"],
|
||||
qwen: ["coder-model"],
|
||||
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
|
||||
zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||
},
|
||||
@@ -68,6 +74,8 @@ const useStore = create<AppState>((set) => ({
|
||||
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
||||
setPRD: (prd) => set({ prd }),
|
||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||
setSelectedModel: (provider, model) =>
|
||||
set((state) => ({
|
||||
@@ -102,6 +110,8 @@ const useStore = create<AppState>((set) => ({
|
||||
enhancedPrompt: null,
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
googleAdsResult: null,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
basePath: '/tools/promptarch',
|
||||
trailingSlash: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
683
package-lock.json
generated
683
package-lock.json
generated
@@ -9,15 +9,17 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^15.0.3",
|
||||
"next": "^16.1.1",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -50,17 +52,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||
@@ -71,16 +62,6 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
|
||||
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||
@@ -764,22 +745,10 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
|
||||
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
|
||||
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -792,9 +761,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
|
||||
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
|
||||
"integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -808,9 +777,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
|
||||
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
|
||||
"integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -824,9 +793,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
|
||||
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
|
||||
"integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -840,9 +809,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
|
||||
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
|
||||
"integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -856,9 +825,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
|
||||
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
|
||||
"integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -872,9 +841,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
|
||||
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
|
||||
"integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -888,9 +857,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
|
||||
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
|
||||
"integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -904,9 +873,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
|
||||
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
|
||||
"integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -963,6 +932,294 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
@@ -1032,16 +1289,6 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
@@ -1188,7 +1435,7 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
@@ -1467,243 +1714,6 @@
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
|
||||
"integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-android-arm64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
|
||||
"integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-darwin-arm64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
|
||||
"integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-darwin-x64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
|
||||
"integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-freebsd-x64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
|
||||
"integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
|
||||
"integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
|
||||
"integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm64-musl": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
|
||||
"integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
|
||||
"integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-x64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-x64-musl": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
|
||||
"integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-wasm32-wasi": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
|
||||
"integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
|
||||
"integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
|
||||
"integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-x64-msvc": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
|
||||
@@ -2324,6 +2334,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
@@ -3499,20 +3521,6 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -5560,13 +5568,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
|
||||
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
|
||||
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.9",
|
||||
"@next/env": "16.1.1",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
@@ -5575,18 +5584,18 @@
|
||||
"next": "dist/bin/next"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.5.7",
|
||||
"@next/swc-darwin-x64": "15.5.7",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.7",
|
||||
"@next/swc-linux-arm64-musl": "15.5.7",
|
||||
"@next/swc-linux-x64-gnu": "15.5.7",
|
||||
"@next/swc-linux-x64-musl": "15.5.7",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.7",
|
||||
"@next/swc-win32-x64-msvc": "15.5.7",
|
||||
"sharp": "^0.34.3"
|
||||
"@next/swc-darwin-arm64": "16.1.1",
|
||||
"@next/swc-darwin-x64": "16.1.1",
|
||||
"@next/swc-linux-arm64-gnu": "16.1.1",
|
||||
"@next/swc-linux-arm64-musl": "16.1.1",
|
||||
"@next/swc-linux-x64-gnu": "16.1.1",
|
||||
"@next/swc-linux-x64-musl": "16.1.1",
|
||||
"@next/swc-win32-arm64-msvc": "16.1.1",
|
||||
"@next/swc-win32-x64-msvc": "16.1.1",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
|
||||
26
package.json
26
package.json
@@ -9,15 +9,17 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^15.0.3",
|
||||
"next": "^16.1.1",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -37,7 +39,23 @@
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
"keywords": [
|
||||
"ai",
|
||||
"prompt-engineering",
|
||||
"prd-generator",
|
||||
"nextjs",
|
||||
"qwen",
|
||||
"ollama",
|
||||
"zai"
|
||||
],
|
||||
"author": "Roman | RyzenAdvanced <https://github.com/roman-ryzenadvanced>",
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer/issues"
|
||||
},
|
||||
"homepage": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer#readme"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -11,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -19,9 +23,19 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
111
types/index.ts
111
types/index.ts
@@ -91,3 +91,114 @@ export interface ChatMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Slide {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
htmlContent: string;
|
||||
notes?: string;
|
||||
layout: "title" | "content" | "two-column" | "image-left" | "image-right" | "quote" | "statistics" | "timeline" | "comparison";
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface SlidesPresentation {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
author?: string;
|
||||
organization?: string;
|
||||
theme: "corporate" | "modern" | "minimal" | "dark" | "vibrant" | "gradient";
|
||||
language: string;
|
||||
slides: Slide[];
|
||||
rawContent: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface GoogleAdsKeyword {
|
||||
keyword: string;
|
||||
type: "primary" | "long-tail" | "negative";
|
||||
searchVolume?: number;
|
||||
competition: "low" | "medium" | "high";
|
||||
difficultyScore?: number;
|
||||
relevanceScore?: number;
|
||||
cpc?: string;
|
||||
}
|
||||
|
||||
export interface GoogleAdCopy {
|
||||
id: string;
|
||||
campaignType: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||
headlines: string[];
|
||||
descriptions: string[];
|
||||
callToAction: string;
|
||||
displayUrl?: string;
|
||||
finalUrl?: string;
|
||||
mobileOptimized: boolean;
|
||||
}
|
||||
|
||||
export interface GoogleAdGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
theme: string;
|
||||
keywords: string[];
|
||||
ads: GoogleAdCopy[];
|
||||
biddingStrategy?: string;
|
||||
}
|
||||
|
||||
export interface GoogleAdsCampaign {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||
budget: {
|
||||
daily?: number;
|
||||
monthly?: number;
|
||||
currency: string;
|
||||
};
|
||||
targeting: {
|
||||
locations?: string[];
|
||||
demographics?: string[];
|
||||
devices?: string[];
|
||||
schedule?: string[];
|
||||
};
|
||||
adGroups: GoogleAdGroup[];
|
||||
}
|
||||
|
||||
export interface GoogleAdsResult {
|
||||
id: string;
|
||||
websiteUrl: string;
|
||||
productsServices: string[];
|
||||
generatedAt: Date;
|
||||
|
||||
// Keyword Research Package
|
||||
keywords: {
|
||||
primary: GoogleAdsKeyword[];
|
||||
longTail: GoogleAdsKeyword[];
|
||||
negative: GoogleAdsKeyword[];
|
||||
};
|
||||
|
||||
// Ad Copy Suite
|
||||
adCopies: GoogleAdCopy[];
|
||||
|
||||
// Campaign Structure
|
||||
campaigns: GoogleAdsCampaign[];
|
||||
|
||||
// Implementation Guidance
|
||||
implementation: {
|
||||
setupSteps: string[];
|
||||
qualityScoreTips: string[];
|
||||
trackingSetup: string[];
|
||||
optimizationTips: string[];
|
||||
};
|
||||
|
||||
// Performance Predictions
|
||||
predictions?: {
|
||||
estimatedClicks?: string;
|
||||
estimatedImpressions?: string;
|
||||
estimatedCtr?: string;
|
||||
estimatedConversions?: string;
|
||||
};
|
||||
|
||||
rawContent: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,8 @@
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": ".next",
|
||||
"framework": "nextjs",
|
||||
"devCommand": "npm run dev"
|
||||
"devCommand": "npm run dev",
|
||||
"env": {
|
||||
"NEXT_PUBLIC_SITE_URL": "https://traetlzlxn2t.vercel.app"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user