Compare commits

...

14 Commits

32 changed files with 2053 additions and 807 deletions

View File

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

15
LICENSE Normal file
View File

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

View File

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

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

View File

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

View File

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

View File

@@ -0,0 +1,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
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
import { QWEN_OAUTH_BASE_URL, createQwenHeaders } from "../constants";
export async function GET(request: NextRequest) {
try {
const authorization = request.headers.get("authorization");
if (!authorization || !authorization.startsWith("Bearer ")) {
return NextResponse.json(
{ error: "Authorization required" },
{ status: 401 }
);
}
const token = authorization.slice(7);
const userResponse = await fetch(`${QWEN_OAUTH_BASE_URL}/api/v1/user`, {
headers: {
...createQwenHeaders(),
Authorization: `Bearer ${token}`,
},
});
if (!userResponse.ok) {
const errorText = await userResponse.text();
let errorData;
try {
errorData = JSON.parse(errorText);
} catch {
errorData = { error: errorText || "Failed to fetch user info" };
}
return NextResponse.json(errorData, { status: userResponse.status });
}
const userData = await userResponse.json();
return NextResponse.json({ user: userData });
} catch (error) {
console.error("Qwen user info failed", error);
return NextResponse.json(
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Failed to fetch user info" },
{ status: 500 }
);
}
}

View File

@@ -65,5 +65,51 @@
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Mobile optimizations */
html {
scroll-behavior: smooth;
-webkit-tap-highlight-color: transparent;
}
/* Better touch targets */
button, a, [role="button"] {
touch-action: manipulation;
}
/* Prevent text selection on buttons */
button {
-webkit-user-select: none;
user-select: none;
}
/* Safe area padding for notched devices */
.safe-area-inset {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
/* Scrollbar styling for mobile-like experience */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground));
} }
} }

View File

@@ -7,6 +7,7 @@ const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "PromptArch - AI Prompt Engineering Platform", title: "PromptArch - AI Prompt Engineering Platform",
description: "Transform vague ideas into production-ready prompts and PRDs", description: "Transform vague ideas into production-ready prompts and PRDs",
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
}; };
export default function RootLayout({ export default function RootLayout({

View File

@@ -6,60 +6,17 @@ import type { View } from "@/components/Sidebar";
import PromptEnhancer from "@/components/PromptEnhancer"; import PromptEnhancer from "@/components/PromptEnhancer";
import PRDGenerator from "@/components/PRDGenerator"; import PRDGenerator from "@/components/PRDGenerator";
import ActionPlanGenerator from "@/components/ActionPlanGenerator"; import ActionPlanGenerator from "@/components/ActionPlanGenerator";
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
import HistoryPanel from "@/components/HistoryPanel"; import HistoryPanel from "@/components/HistoryPanel";
import SettingsPanel from "@/components/SettingsPanel"; import SettingsPanel from "@/components/SettingsPanel";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance"; import modelAdapter from "@/lib/services/adapter-instance";
export default function Home() { export default function Home() {
const [currentView, setCurrentView] = useState<View>("enhance"); const [currentView, setCurrentView] = useState<View>("enhance");
const { setQwenTokens, setApiKey } = useStore();
useEffect(() => { useEffect(() => {
// Handle OAuth callback console.log("[Home] Initializing Qwen OAuth service on client...");
if (typeof window !== "undefined") { modelAdapter["qwenService"]["initialize"]?.();
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (code) {
// In a real app, you would exchange the code for tokens here
// Since we don't have a backend or real client secret, we'll simulate it
console.log("OAuth code received:", code);
// Mock token exchange
const mockAccessToken = "mock_access_token_" + Math.random().toString(36).substr(2, 9);
const tokens = {
accessToken: mockAccessToken,
expiresAt: Date.now() + 3600 * 1000, // 1 hour
};
setQwenTokens(tokens);
modelAdapter.setQwenOAuthTokens(tokens.accessToken, undefined, 3600);
// Save to localStorage
localStorage.setItem("promptarch-qwen-tokens", JSON.stringify(tokens));
// Clear the code from URL
window.history.replaceState({}, document.title, window.location.pathname);
// Switch to settings to show success (optional)
setCurrentView("settings");
}
// Load tokens from localStorage on init
const savedTokens = localStorage.getItem("promptarch-qwen-tokens");
if (savedTokens) {
try {
const tokens = JSON.parse(savedTokens);
if (tokens.expiresAt > Date.now()) {
setQwenTokens(tokens);
modelAdapter.setQwenOAuthTokens(tokens.accessToken, tokens.refreshToken, (tokens.expiresAt - Date.now()) / 1000);
}
} catch (e) {
console.error("Failed to load Qwen tokens:", e);
}
}
}
}, []); }, []);
const renderContent = () => { const renderContent = () => {
@@ -70,6 +27,8 @@ export default function Home() {
return <PRDGenerator />; return <PRDGenerator />;
case "action": case "action":
return <ActionPlanGenerator />; return <ActionPlanGenerator />;
case "uxdesigner":
return <UXDesignerPrompt />;
case "history": case "history":
return <HistoryPanel />; return <HistoryPanel />;
case "settings": case "settings":
@@ -82,7 +41,7 @@ export default function Home() {
return ( return (
<div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<Sidebar currentView={currentView} onViewChange={setCurrentView} /> <Sidebar currentView={currentView} onViewChange={setCurrentView} />
<main className="flex-1 overflow-auto p-8"> <main className="flex-1 overflow-auto pt-16 lg:pt-0 px-4 py-4 lg:p-8">
<div className="mx-auto max-w-7xl"> <div className="mx-auto max-w-7xl">
{renderContent()} {renderContent()}
</div> </div>

View File

@@ -71,7 +71,9 @@ export default function ActionPlanGenerator() {
} }
const apiKey = apiKeys[selectedProvider]; const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`); setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return; return;
} }
@@ -79,9 +81,13 @@ export default function ActionPlanGenerator() {
setProcessing(true); setProcessing(true);
setError(null); setError(null);
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try { try {
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel); const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
console.log("[ActionPlanGenerator] Generation result:", result);
if (result.success && result.data) { if (result.success && result.data) {
const newPlan = { const newPlan = {
id: Math.random().toString(36).substr(2, 9), id: Math.random().toString(36).substr(2, 9),
@@ -100,9 +106,11 @@ export default function ActionPlanGenerator() {
}; };
setActionPlan(newPlan); setActionPlan(newPlan);
} else { } else {
console.error("[ActionPlanGenerator] Generation failed:", result.error);
setError(result.error || "Failed to generate action plan"); setError(result.error || "Failed to generate action plan");
} }
} catch (err) { } catch (err) {
console.error("[ActionPlanGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred"); setError(err instanceof Error ? err.message : "An error occurred");
} finally { } finally {
setProcessing(false); setProcessing(false);
@@ -118,28 +126,28 @@ export default function ActionPlanGenerator() {
}; };
return ( return (
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2"> <div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
<Card className="h-fit"> <Card className="h-fit">
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<ListTodo className="h-5 w-5" /> <ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
Action Plan Generator Action Plan Generator
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Convert PRD into actionable implementation plan Convert PRD into actionable implementation plan
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label> <label className="text-xs lg:text-sm font-medium">AI Provider</label>
<div className="flex gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button <Button
key={provider} key={provider}
variant={selectedProvider === provider ? "default" : "outline"} variant={selectedProvider === provider ? "default" : "outline"}
size="sm" size="sm"
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className="capitalize" className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
> >
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"} {provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button> </Button>
@@ -148,11 +156,11 @@ export default function ActionPlanGenerator() {
</div> </div>
<div className="space-y-2"> <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 <select
value={selectedModel} value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)} onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
{models.map((model) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
@@ -163,36 +171,36 @@ export default function ActionPlanGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">PRD / Requirements</label> <label className="text-xs lg:text-sm font-medium">PRD / Requirements</label>
<Textarea <Textarea
placeholder="Paste your PRD or project requirements here..." placeholder="Paste your PRD or project requirements here..."
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y" className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
/> />
</div> </div>
{error && ( {error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error} {error}
{!apiKeys[selectedProvider] && ( {!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2"> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-xs">Configure API key in Settings</span> <span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
</div> </div>
)} )}
</div> </div>
)} )}
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full"> <Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
Generating Action Plan... 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 Generate Action Plan
</> </>
)} )}
@@ -201,43 +209,43 @@ export default function ActionPlanGenerator() {
</Card> </Card>
<Card className={cn(!actionPlan && "opacity-50")}> <Card className={cn(!actionPlan && "opacity-50")}>
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
Action Plan Action Plan
</span> </span>
{actionPlan && ( {actionPlan && (
<Button variant="ghost" size="icon" onClick={handleCopy}> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? ( {copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
)} )}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Task breakdown, frameworks, and architecture recommendations Task breakdown, frameworks, and architecture recommendations
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{actionPlan ? ( {actionPlan ? (
<div className="space-y-4"> <div className="space-y-3 lg:space-y-4">
<div className="rounded-md border bg-primary/5 p-4"> <div className="rounded-md border bg-primary/5 p-3 lg:p-4">
<h4 className="mb-2 flex items-center gap-2 font-semibold"> <h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
<Clock className="h-4 w-4" /> <Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Implementation Roadmap Implementation Roadmap
</h4> </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>
<div className="rounded-md border bg-muted/30 p-4"> <div className="rounded-md border bg-muted/30 p-3 lg:p-4">
<h4 className="mb-2 flex items-center gap-2 font-semibold"> <h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Quick Notes Quick Notes
</h4> </h4>
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground"> <ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
<li>Review all task dependencies before starting</li> <li>Review all task dependencies before starting</li>
<li>Set up recommended framework architecture</li> <li>Set up recommended framework architecture</li>
<li>Follow best practices for security and performance</li> <li>Follow best practices for security and performance</li>
@@ -246,7 +254,7 @@ export default function ActionPlanGenerator() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground"> <div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
Action plan will appear here Action plan will appear here
</div> </div>
)} )}

View File

@@ -4,7 +4,6 @@ import useStore from "@/lib/store";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Clock, Trash2, RotateCcw } from "lucide-react"; import { Clock, Trash2, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
export default function HistoryPanel() { export default function HistoryPanel() {
const { history, setCurrentPrompt, clearHistory } = useStore(); const { history, setCurrentPrompt, clearHistory } = useStore();
@@ -22,11 +21,11 @@ export default function HistoryPanel() {
if (history.length === 0) { if (history.length === 0) {
return ( return (
<Card> <Card>
<CardContent className="flex h-[400px] items-center justify-center"> <CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6">
<div className="text-center"> <div className="text-center">
<Clock className="mx-auto h-12 w-12 text-muted-foreground/50" /> <Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
<p className="mt-4 text-muted-foreground">No history yet</p> <p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground">No history yet</p>
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
Start enhancing prompts to see them here Start enhancing prompts to see them here
</p> </p>
</div> </div>
@@ -37,35 +36,35 @@ export default function HistoryPanel() {
return ( return (
<Card> <Card>
<CardHeader className="flex-row items-center justify-between"> <CardHeader className="flex-row items-center justify-between p-4 lg:p-6">
<div> <div>
<CardTitle>History</CardTitle> <CardTitle className="text-base lg:text-lg">History</CardTitle>
<CardDescription>{history.length} items</CardDescription> <CardDescription className="text-xs lg:text-sm">{history.length} items</CardDescription>
</div> </div>
<Button variant="outline" size="icon" onClick={handleClear}> <Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9">
<Trash2 className="h-4 w-4" /> <Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-2 lg:space-y-3 p-4 lg:p-6 pt-0 lg:pt-0">
{history.map((item) => ( {history.map((item) => (
<div <div
key={item.id} key={item.id}
className="rounded-md border bg-muted/30 p-4 transition-colors hover:bg-muted/50" className="rounded-md border bg-muted/30 p-3 lg:p-4 transition-colors hover:bg-muted/50"
> >
<div className="mb-2 flex items-center justify-between"> <div className="mb-1.5 lg:mb-2 flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground"> <span className="text-[10px] lg:text-xs text-muted-foreground truncate">
{new Date(item.timestamp).toLocaleString()} {new Date(item.timestamp).toLocaleString()}
</span> </span>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-6 w-6" className="h-6 w-6 flex-shrink-0"
onClick={() => handleRestore(item.prompt)} onClick={() => handleRestore(item.prompt)}
> >
<RotateCcw className="h-3 w-3" /> <RotateCcw className="h-3 w-3" />
</Button> </Button>
</div> </div>
<p className="line-clamp-3 text-sm">{item.prompt}</p> <p className="line-clamp-3 text-xs lg:text-sm">{item.prompt}</p>
</div> </div>
))} ))}
</CardContent> </CardContent>

View File

@@ -78,7 +78,9 @@ export default function PRDGenerator() {
} }
const apiKey = apiKeys[selectedProvider]; const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`); setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return; return;
} }
@@ -86,9 +88,13 @@ export default function PRDGenerator() {
setProcessing(true); setProcessing(true);
setError(null); setError(null);
console.log("[PRDGenerator] Starting PRD generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try { try {
const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel); const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel);
console.log("[PRDGenerator] Generation result:", result);
if (result.success && result.data) { if (result.success && result.data) {
const newPRD = { const newPRD = {
id: Math.random().toString(36).substr(2, 9), id: Math.random().toString(36).substr(2, 9),
@@ -105,9 +111,11 @@ export default function PRDGenerator() {
}; };
setPRD(newPRD); setPRD(newPRD);
} else { } else {
console.error("[PRDGenerator] Generation failed:", result.error);
setError(result.error || "Failed to generate PRD"); setError(result.error || "Failed to generate PRD");
} }
} catch (err) { } catch (err) {
console.error("[PRDGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred"); setError(err instanceof Error ? err.message : "An error occurred");
} finally { } finally {
setProcessing(false); setProcessing(false);
@@ -132,28 +140,28 @@ export default function PRDGenerator() {
]; ];
return ( return (
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2"> <div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
<Card className="h-fit"> <Card className="h-fit">
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<FileText className="h-5 w-5" /> <FileText className="h-4 w-4 lg:h-5 lg:w-5" />
PRD Generator PRD Generator
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Generate comprehensive Product Requirements Document from your idea Generate comprehensive Product Requirements Document from your idea
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label> <label className="text-xs lg:text-sm font-medium">AI Provider</label>
<div className="flex gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button <Button
key={provider} key={provider}
variant={selectedProvider === provider ? "default" : "outline"} variant={selectedProvider === provider ? "default" : "outline"}
size="sm" size="sm"
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className="capitalize" className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
> >
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"} {provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button> </Button>
@@ -162,11 +170,11 @@ export default function PRDGenerator() {
</div> </div>
<div className="space-y-2"> <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 <select
value={selectedModel} value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)} onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
{models.map((model) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
@@ -177,36 +185,36 @@ export default function PRDGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Your Idea</label> <label className="text-xs lg:text-sm font-medium">Your Idea</label>
<Textarea <Textarea
placeholder="e.g., A task management app with real-time collaboration features" placeholder="e.g., A task management app with real-time collaboration features"
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y" className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
/> />
</div> </div>
{error && ( {error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error} {error}
{!apiKeys[selectedProvider] && ( {!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2"> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-xs">Configure API key in Settings</span> <span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
</div> </div>
)} )}
</div> </div>
)} )}
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full"> <Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
Generating PRD... 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 Generate PRD
</> </>
)} )}
@@ -215,52 +223,52 @@ export default function PRDGenerator() {
</Card> </Card>
<Card className={cn(!prd && "opacity-50")}> <Card className={cn(!prd && "opacity-50")}>
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
Generated PRD Generated PRD
</span> </span>
{prd && ( {prd && (
<Button variant="ghost" size="icon" onClick={handleCopy}> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? ( {copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
)} )}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Structured requirements document ready for development Structured requirements document ready for development
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{prd ? ( {prd ? (
<div className="space-y-3"> <div className="space-y-2 lg:space-y-3">
{sections.map((section) => ( {sections.map((section) => (
<div key={section.id} className="rounded-md border bg-muted/30"> <div key={section.id} className="rounded-md border bg-muted/30">
<button <button
onClick={() => toggleSection(section.id)} onClick={() => toggleSection(section.id)}
className="flex w-full items-center justify-between px-4 py-3 text-left font-medium transition-colors hover:bg-muted/50" className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
> >
<span>{section.title}</span> <span>{section.title}</span>
{expandedSections.includes(section.id) ? ( {expandedSections.includes(section.id) ? (
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</button> </button>
{expandedSections.includes(section.id) && ( {expandedSections.includes(section.id) && (
<div className="border-t bg-background px-4 py-3"> <div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3">
<pre className="whitespace-pre-wrap text-sm">{prd.overview}</pre> <pre className="whitespace-pre-wrap text-xs lg:text-sm">{prd.overview}</pre>
</div> </div>
)} )}
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground"> <div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
PRD will appear here PRD will appear here
</div> </div>
)} )}

View File

@@ -71,7 +71,9 @@ export default function PromptEnhancer() {
} }
const apiKey = apiKeys[selectedProvider]; const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`); setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return; return;
} }
@@ -79,15 +81,21 @@ export default function PromptEnhancer() {
setProcessing(true); setProcessing(true);
setError(null); setError(null);
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try { try {
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel); const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[PromptEnhancer] Enhancement result:", result);
if (result.success && result.data) { if (result.success && result.data) {
setEnhancedPrompt(result.data); setEnhancedPrompt(result.data);
} else { } else {
console.error("[PromptEnhancer] Enhancement failed:", result.error);
setError(result.error || "Failed to enhance prompt"); setError(result.error || "Failed to enhance prompt");
} }
} catch (err) { } catch (err) {
console.error("[PromptEnhancer] Enhancement error:", err);
setError(err instanceof Error ? err.message : "An error occurred"); setError(err instanceof Error ? err.message : "An error occurred");
} finally { } finally {
setProcessing(false); setProcessing(false);
@@ -109,21 +117,21 @@ export default function PromptEnhancer() {
}; };
return ( return (
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2"> <div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
<Card className="h-fit"> <Card className="h-fit">
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Sparkles className="h-5 w-5" /> <Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
Input Prompt Input Prompt
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Enter your prompt and we'll enhance it for AI coding agents Enter your prompt and we'll enhance it for AI coding agents
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label> <label className="text-xs lg:text-sm font-medium">AI Provider</label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button <Button
key={provider} key={provider}
@@ -131,7 +139,7 @@ export default function PromptEnhancer() {
size="sm" size="sm"
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className={cn( className={cn(
"capitalize", "capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground" selectedProvider === provider && "bg-primary text-primary-foreground"
)} )}
> >
@@ -142,11 +150,11 @@ export default function PromptEnhancer() {
</div> </div>
<div className="space-y-2"> <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 <select
value={selectedModel} value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)} onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
{models.map((model) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
@@ -157,77 +165,77 @@ export default function PromptEnhancer() {
</div> </div>
<div className="space-y-2"> <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 <Textarea
placeholder="e.g., Create a user authentication system with JWT tokens" placeholder="e.g., Create a user authentication system with JWT tokens"
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y" className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
/> />
</div> </div>
{error && ( {error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error} {error}
{!apiKeys[selectedProvider] && ( {!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2"> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-xs">Configure API key in Settings</span> <span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
</div> </div>
)} )}
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1"> <Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
Enhancing... 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 Enhance Prompt
</> </>
)} )}
</Button> </Button>
<Button variant="outline" onClick={handleClear} disabled={isProcessing}> <Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
Clear <span className="hidden sm:inline">Clear</span>
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className={cn(!enhancedPrompt && "opacity-50")}> <Card className={cn(!enhancedPrompt && "opacity-50")}>
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
Enhanced Prompt Enhanced Prompt
</span> </span>
{enhancedPrompt && ( {enhancedPrompt && (
<Button variant="ghost" size="icon" onClick={handleCopy}> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? ( {copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
)} )}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Professional prompt ready for coding agents Professional prompt ready for coding agents
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{enhancedPrompt ? ( {enhancedPrompt ? (
<div className="rounded-md border bg-muted/50 p-4"> <div className="rounded-md border bg-muted/50 p-3 lg:p-4">
<pre className="whitespace-pre-wrap text-sm">{enhancedPrompt}</pre> <pre className="whitespace-pre-wrap text-xs lg:text-sm">{enhancedPrompt}</pre>
</div> </div>
) : ( ) : (
<div className="flex h-[200px] items-center justify-center text-center text-sm text-muted-foreground"> <div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
Enhanced prompt will appear here Enhanced prompt will appear here
</div> </div>
)} )}

View File

@@ -3,16 +3,15 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import useStore from "@/lib/store"; import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance"; import modelAdapter from "@/lib/services/adapter-instance";
import { Save, Key, Server, Eye, EyeOff } from "lucide-react"; import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
export default function SettingsPanel() { export default function SettingsPanel() {
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore(); const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({}); const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
const [isAuthLoading, setIsAuthLoading] = useState(false);
const handleSave = () => { const handleSave = () => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -43,6 +42,10 @@ export default function SettingsPanel() {
console.error("Failed to load API keys:", e); console.error("Failed to load API keys:", e);
} }
} }
const storedTokens = modelAdapter.getQwenTokenInfo();
if (storedTokens) {
setQwenTokens(storedTokens);
}
} }
}; };
@@ -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(() => { useEffect(() => {
handleLoad(); handleLoad();
}, []); }, []);
return ( return (
<div className="mx-auto max-w-3xl space-y-6"> <div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
<Card> <Card>
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Key className="h-5 w-5" /> <Key className="h-4 w-4 lg:h-5 lg:w-5" />
API Configuration API Configuration
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Configure API keys for different AI providers Configure API keys for different AI providers
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium"> <label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-4 w-4" /> <Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Qwen Code API Key Qwen Code API Key
</label> </label>
<div className="relative"> <div className="relative">
@@ -90,24 +116,24 @@ export default function SettingsPanel() {
placeholder="Enter your Qwen API key" placeholder="Enter your Qwen API key"
value={apiKeys.qwen || ""} value={apiKeys.qwen || ""}
onChange={(e) => handleApiKeyChange("qwen", e.target.value)} onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
className="font-mono text-sm" className="font-mono text-xs lg:text-sm pr-10"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-0 top-0 h-full" className="absolute right-0 top-0 h-full w-9 lg:w-10"
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))} onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
> >
{showApiKey.qwen ? ( {showApiKey.qwen ? (
<EyeOff className="h-4 w-4" /> <EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<Eye className="h-4 w-4" /> <Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
<p className="text-xs text-muted-foreground flex-1"> <p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
Get API key from{" "} Get API key from{" "}
<a <a
href="https://help.aliyun.com/zh/dashscope/" href="https://help.aliyun.com/zh/dashscope/"
@@ -121,30 +147,27 @@ export default function SettingsPanel() {
<Button <Button
variant={qwenTokens ? "secondary" : "outline"} variant={qwenTokens ? "secondary" : "outline"}
size="sm" size="sm"
className="h-8" className="h-7 lg:h-8 text-[10px] lg:text-xs w-full sm:w-auto"
onClick={() => { onClick={handleQwenAuth}
if (qwenTokens) { disabled={isAuthLoading}
setQwenTokens(undefined as any);
localStorage.removeItem("promptarch-qwen-tokens");
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
} else {
window.location.href = modelAdapter.getQwenAuthUrl();
}
}}
> >
{qwenTokens ? "Logout from Qwen" : "Login with Qwen (OAuth)"} {isAuthLoading
? "Signing in..."
: qwenTokens
? "Logout from Qwen"
: "Login with Qwen (OAuth)"}
</Button> </Button>
</div> </div>
{qwenTokens && ( {qwenTokens && (
<p className="text-[10px] text-green-600 dark:text-green-400 font-medium"> <p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()}) Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
</p> </p>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium"> <label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-4 w-4" /> <Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Ollama Cloud API Key Ollama Cloud API Key
</label> </label>
<div className="relative"> <div className="relative">
@@ -153,23 +176,23 @@ export default function SettingsPanel() {
placeholder="Enter your Ollama API key" placeholder="Enter your Ollama API key"
value={apiKeys.ollama || ""} value={apiKeys.ollama || ""}
onChange={(e) => handleApiKeyChange("ollama", e.target.value)} onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
className="font-mono text-sm" className="font-mono text-xs lg:text-sm pr-10"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-0 top-0 h-full" className="absolute right-0 top-0 h-full w-9 lg:w-10"
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))} onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
> >
{showApiKey.ollama ? ( {showApiKey.ollama ? (
<EyeOff className="h-4 w-4" /> <EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<Eye className="h-4 w-4" /> <Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-[10px] lg:text-xs text-muted-foreground">
Get API key from{" "} Get API key from{" "}
<a <a
href="https://ollama.com/cloud" href="https://ollama.com/cloud"
@@ -183,8 +206,8 @@ export default function SettingsPanel() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium"> <label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-4 w-4" /> <Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
Z.AI Plan API Key Z.AI Plan API Key
</label> </label>
<div className="relative"> <div className="relative">
@@ -193,23 +216,23 @@ export default function SettingsPanel() {
placeholder="Enter your Z.AI API key" placeholder="Enter your Z.AI API key"
value={apiKeys.zai || ""} value={apiKeys.zai || ""}
onChange={(e) => handleApiKeyChange("zai", e.target.value)} onChange={(e) => handleApiKeyChange("zai", e.target.value)}
className="font-mono text-sm" className="font-mono text-xs lg:text-sm pr-10"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-0 top-0 h-full" className="absolute right-0 top-0 h-full w-9 lg:w-10"
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))} onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
> >
{showApiKey.zai ? ( {showApiKey.zai ? (
<EyeOff className="h-4 w-4" /> <EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : ( ) : (
<Eye className="h-4 w-4" /> <Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </Button>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-[10px] lg:text-xs text-muted-foreground">
Get API key from{" "} Get API key from{" "}
<a <a
href="https://docs.z.ai" href="https://docs.z.ai"
@@ -222,45 +245,44 @@ export default function SettingsPanel() {
</p> </p>
</div> </div>
<Button onClick={handleSave} className="w-full"> <Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
<Save className="mr-2 h-4 w-4" /> <Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
Save API Keys Save API Keys
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle>Default Provider</CardTitle> <CardTitle className="text-base lg:text-lg">Default Provider</CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Select your preferred AI provider Select your preferred AI provider
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="grid gap-3"> <div className="grid gap-2 lg:gap-3">
{(["qwen", "ollama", "zai"] as const).map((provider) => ( {(["qwen", "ollama", "zai"] as const).map((provider) => (
<button <button
key={provider} key={provider}
onClick={() => setSelectedProvider(provider)} onClick={() => setSelectedProvider(provider)}
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 ${ className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg:p-4 text-left transition-colors hover:bg-muted/50 ${selectedProvider === provider
selectedProvider === provider
? "border-primary bg-primary/5" ? "border-primary bg-primary/5"
: "border-border" : "border-border"
}`} }`}
> >
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10"> <div className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
<Server className="h-5 w-5 text-primary" /> <Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
</div> </div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<h3 className="font-medium capitalize">{provider}</h3> <h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
<p className="text-sm text-muted-foreground"> <p className="text-[10px] lg:text-sm text-muted-foreground truncate">
{provider === "qwen" && "Alibaba DashScope API"} {provider === "qwen" && "Alibaba DashScope API"}
{provider === "ollama" && "Ollama Cloud API"} {provider === "ollama" && "Ollama Cloud API"}
{provider === "zai" && "Z.AI Plan API"} {provider === "zai" && "Z.AI Plan API"}
</p> </p>
</div> </div>
{selectedProvider === provider && ( {selectedProvider === provider && (
<div className="h-2 w-2 rounded-full bg-primary" /> <div className="h-2 w-2 rounded-full bg-primary flex-shrink-0" />
)} )}
</button> </button>
))} ))}
@@ -269,15 +291,15 @@ export default function SettingsPanel() {
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader className="p-4 lg:p-6">
<CardTitle>Data Privacy</CardTitle> <CardTitle className="text-base lg:text-lg">Data Privacy</CardTitle>
<CardDescription> <CardDescription className="text-xs lg:text-sm">
Your data handling preferences Your data handling preferences
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
<div className="rounded-md border bg-muted/30 p-4"> <div className="rounded-md border bg-muted/30 p-3 lg:p-4">
<p className="text-sm"> <p className="text-xs lg:text-sm">
All API keys are stored locally in your browser. Your prompts are sent directly to the selected AI provider and are not stored by PromptArch. 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> </p>
</div> </div>

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import useStore from "@/lib/store"; import useStore from "@/lib/store";
import { Sparkles, FileText, ListTodo, Settings, History } from "lucide-react"; import { Sparkles, FileText, ListTodo, Palette, History, Settings, Github, Menu, X } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export type View = "enhance" | "prd" | "action" | "history" | "settings"; export type View = "enhance" | "prd" | "action" | "uxdesigner" | "history" | "settings";
interface SidebarProps { interface SidebarProps {
currentView: View; currentView: View;
@@ -14,76 +15,137 @@ interface SidebarProps {
export default function Sidebar({ currentView, onViewChange }: SidebarProps) { export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
const history = useStore((state) => state.history); const history = useStore((state) => state.history);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const menuItems = [ const menuItems = [
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles }, { id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
{ id: "prd" as View, label: "PRD Generator", icon: FileText }, { id: "prd" as View, label: "PRD Generator", icon: FileText },
{ id: "action" as View, label: "Action Plan", icon: ListTodo }, { id: "action" as View, label: "Action Plan", icon: ListTodo },
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
{ id: "history" as View, label: "History", icon: History, count: history.length }, { id: "history" as View, label: "History", icon: History, count: history.length },
{ id: "settings" as View, label: "Settings", icon: Settings }, { id: "settings" as View, label: "Settings", icon: Settings },
]; ];
return ( const handleViewChange = (view: View) => {
<aside className="flex h-screen w-64 flex-col border-r bg-card"> onViewChange(view);
<div className="border-b p-6"> setIsMobileMenuOpen(false);
<h1 className="flex items-center gap-2 text-xl font-bold"> };
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
PA const SidebarContent = () => (
</div> <>
PromptArch <div className="border-b p-4 lg:p-6">
</h1> <a href="https://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-primary text-primary-foreground text-sm lg:text-base">
PA
</div>
PromptArch
</h1>
</a>
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
<span>View on GitHub</span>
</a>
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
Forked from <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
</p>
</div> </div>
<nav className="flex-1 space-y-1 p-4"> <nav className="flex-1 space-y-1 p-3 lg:p-4 overflow-y-auto">
{menuItems.map((item) => ( {menuItems.map((item) => (
<Button <Button
key={item.id} key={item.id}
variant={currentView === item.id ? "default" : "ghost"} variant={currentView === item.id ? "default" : "ghost"}
className={cn( className={cn(
"w-full justify-start gap-2", "w-full justify-start gap-2 h-9 lg:h-10 text-sm",
currentView === item.id && "bg-primary text-primary-foreground" currentView === item.id && "bg-primary text-primary-foreground"
)} )}
onClick={() => onViewChange(item.id)} onClick={() => handleViewChange(item.id)}
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
<span className="flex-1 text-left">{item.label}</span> <span className="flex-1 text-left truncate">{item.label}</span>
{item.count !== undefined && item.count > 0 && ( {item.count !== undefined && item.count > 0 && (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium"> <span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium text-primary">
{item.count} {item.count}
</span> </span>
)} )}
</Button> </Button>
))} ))}
<div className="mt-8 p-3 text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-4"> <div className="mt-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> <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> <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>
<p> <p>
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a> Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
</p> </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. 100% Developed using GLM 4.7 model on TRAE.AI IDE.
</p> </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> Model Info: <a href="https://z.ai/subscribe?ic=R0K78RJKNW" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Learn here</a>
</p> </p>
</div> </div>
</div> </div>
</nav> </nav>
<div className="border-t p-4"> <div className="border-t p-3 lg:p-4 hidden lg:block">
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground"> <div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
<p className="font-medium text-foreground">Quick Tips</p> <p className="font-medium text-foreground">Quick Tips</p>
<ul className="mt-2 space-y-1"> <ul className="mt-1.5 lg:mt-2 space-y-0.5 lg:space-y-1">
<li> Use different providers for best results</li> <li> Use different providers for best results</li>
<li> Copy enhanced prompts to your AI agent</li> <li> Copy enhanced prompts to your AI agent</li>
<li> PRDs generate better action plans</li> <li> PRDs generate better action plans</li>
</ul> </ul>
</div> </div>
</div> </div>
</aside> </>
);
return (
<>
{/* Mobile Header */}
<div className="lg:hidden fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b bg-card px-4 py-3">
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
PA
</div>
<span className="font-bold text-lg">PromptArch</span>
</a>
<Button
variant="ghost"
size="icon"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="h-9 w-9"
>
{isMobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
</div>
{/* Mobile Menu Overlay */}
{isMobileMenuOpen && (
<div
className="lg:hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Mobile Slide-out Menu */}
<aside
className={cn(
"lg:hidden fixed top-0 left-0 z-50 flex h-full w-72 max-w-[80vw] flex-col border-r bg-card transition-transform duration-300 ease-in-out",
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<SidebarContent />
</aside>
{/* Desktop Sidebar */}
<aside className="hidden lg:flex h-screen w-64 flex-col border-r bg-card flex-shrink-0">
<SidebarContent />
</aside>
</>
); );
} }

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

View File

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

View File

@@ -1,13 +1,10 @@
import type { ModelProvider, APIResponse, ChatMessage } from "@/types"; import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
import QwenOAuthService from "./qwen-oauth";
import OllamaCloudService from "./ollama-cloud"; import OllamaCloudService from "./ollama-cloud";
import ZaiPlanService from "./zai-plan"; import ZaiPlanService from "./zai-plan";
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
export interface ModelAdapterConfig { export interface ModelAdapterConfig {
qwen?: { qwen?: QwenOAuthConfig;
apiKey?: string;
endpoint?: string;
};
ollama?: { ollama?: {
apiKey?: string; apiKey?: string;
endpoint?: string; endpoint?: string;
@@ -20,34 +17,35 @@ export interface ModelAdapterConfig {
} }
export class ModelAdapter { export class ModelAdapter {
private qwenService: QwenOAuthService;
private ollamaService: OllamaCloudService; private ollamaService: OllamaCloudService;
private zaiService: ZaiPlanService; private zaiService: ZaiPlanService;
private qwenService = qwenOAuthService;
private preferredProvider: ModelProvider; private preferredProvider: ModelProvider;
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "qwen") { constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "ollama") {
this.qwenService = new QwenOAuthService(config.qwen);
this.ollamaService = new OllamaCloudService(config.ollama); this.ollamaService = new OllamaCloudService(config.ollama);
this.zaiService = new ZaiPlanService(config.zai); this.zaiService = new ZaiPlanService(config.zai);
this.preferredProvider = preferredProvider; this.preferredProvider = preferredProvider;
if (config.qwen) {
if (config.qwen.apiKey) {
this.qwenService.setApiKey(config.qwen.apiKey);
}
if (config.qwen.accessToken) {
this.qwenService.setOAuthTokens({
accessToken: config.qwen.accessToken,
refreshToken: config.qwen.refreshToken,
expiresAt: config.qwen.expiresAt,
resourceUrl: config.qwen.resourceUrl,
});
}
}
} }
setPreferredProvider(provider: ModelProvider): void { setPreferredProvider(provider: ModelProvider): void {
this.preferredProvider = provider; this.preferredProvider = provider;
} }
updateQwenApiKey(apiKey: string): void {
this.qwenService = new QwenOAuthService({ apiKey });
}
setQwenOAuthTokens(accessToken: string, refreshToken?: string, expiresIn?: number): void {
this.qwenService.setOAuthTokens(accessToken, refreshToken, expiresIn);
}
getQwenAuthUrl(): string {
return this.qwenService.getAuthorizationUrl();
}
updateOllamaApiKey(apiKey: string): void { updateOllamaApiKey(apiKey: string): void {
this.ollamaService = new OllamaCloudService({ apiKey }); this.ollamaService = new OllamaCloudService({ apiKey });
} }
@@ -56,17 +54,77 @@ export class ModelAdapter {
this.zaiService = new ZaiPlanService({ apiKey }); this.zaiService = new ZaiPlanService({ apiKey });
} }
updateQwenApiKey(apiKey: string): void {
this.qwenService.setApiKey(apiKey);
}
updateQwenTokens(tokens?: QwenOAuthToken): void {
this.qwenService.setOAuthTokens(tokens);
}
async startQwenOAuth(): Promise<QwenOAuthToken> {
return await this.qwenService.signIn();
}
getQwenTokenInfo(): QwenOAuthToken | null {
return this.qwenService.getTokenInfo();
}
hasQwenAuth(): boolean {
return this.qwenService.hasOAuthToken();
}
private isProviderAuthenticated(provider: ModelProvider): boolean {
switch (provider) {
case "qwen":
return this.hasQwenAuth() || this.qwenService.hasApiKey();
case "ollama":
return this.ollamaService.hasAuth();
case "zai":
return this.zaiService.hasAuth();
default:
return false;
}
}
private buildFallbackProviders(...providers: ModelProvider[]): ModelProvider[] {
const seen = new Set<ModelProvider>();
return providers.filter((provider) => {
if (seen.has(provider)) {
return false;
}
seen.add(provider);
return true;
});
}
private async callWithFallback<T>( private async callWithFallback<T>(
operation: (service: any) => Promise<APIResponse<T>>, operation: (service: any) => Promise<APIResponse<T>>,
providers: ModelProvider[] providers: ModelProvider[]
): Promise<APIResponse<T>> { ): Promise<APIResponse<T>> {
console.log("[ModelAdapter] Attempting providers in order:", providers);
let lastError: string | null = null;
for (const provider of providers) { for (const provider of providers) {
try { try {
console.log(`[ModelAdapter] Checking authentication for ${provider}...`);
if (!this.isProviderAuthenticated(provider)) {
console.log(`[ModelAdapter] Provider ${provider} is not authenticated, skipping`);
continue;
}
let service: any; let service: any;
console.log(`[ModelAdapter] Trying provider: ${provider}`);
switch (provider) { switch (provider) {
case "qwen": case "qwen":
service = this.qwenService; service = this.qwenService;
console.log("[ModelAdapter] Qwen service:", {
hasApiKey: !!this.qwenService["apiKey"],
hasToken: !!this.qwenService.getTokenInfo()?.accessToken
});
break; break;
case "ollama": case "ollama":
service = this.ollamaService; service = this.ollamaService;
@@ -77,35 +135,57 @@ export class ModelAdapter {
} }
const result = await operation(service); const result = await operation(service);
console.log(`[ModelAdapter] Provider ${provider} result:`, result);
if (result.success) { if (result.success) {
console.log(`[ModelAdapter] Success with provider: ${provider}`);
return result; return result;
} }
if (result.error) {
lastError = result.error;
}
} catch (error) { } catch (error) {
console.error(`Error with ${provider}:`, error); const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[ModelAdapter] Error with ${provider}:`, errorMessage);
lastError = errorMessage || lastError;
} }
} }
const finalError = lastError
? `All providers failed: ${lastError}`
: "All providers failed. Please configure API key in Settings";
console.error(`[ModelAdapter] ${finalError}`);
return { return {
success: false, success: false,
error: "All providers failed", error: finalError,
}; };
} }
async enhancePrompt(prompt: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> { 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); return this.callWithFallback((service) => service.enhancePrompt(prompt, model), providers);
} }
async generatePRD(idea: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> { 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); return this.callWithFallback((service) => service.generatePRD(idea, model), providers);
} }
async generateActionPlan(prd: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> { 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); 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 chatCompletion( async chatCompletion(
messages: ChatMessage[], messages: ChatMessage[],
model: string, model: string,
@@ -137,7 +217,7 @@ export class ModelAdapter {
async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> { async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> {
const fallbackModels: Record<ModelProvider, string[]> = { const fallbackModels: Record<ModelProvider, string[]> = {
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"], qwen: this.qwenService.getAvailableModels(),
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"], ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"], zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
}; };
@@ -163,16 +243,6 @@ export class ModelAdapter {
console.error("[ModelAdapter] Failed to load Z.AI models, using fallback:", error); console.error("[ModelAdapter] Failed to load Z.AI models, using fallback:", error);
} }
} }
if (provider === "qwen" || !provider) {
try {
const qwenModels = await this.qwenService.listModels();
if (qwenModels.success && qwenModels.data && qwenModels.data.length > 0) {
models.qwen = qwenModels.data;
}
} catch (error) {
console.error("[ModelAdapter] Failed to load Qwen models, using fallback:", error);
}
}
return { success: true, data: models }; return { success: true, data: models };
} }

View File

@@ -5,11 +5,42 @@ export interface OllamaCloudConfig {
endpoint?: string; endpoint?: string;
} }
export interface OllamaModel { const LOCAL_MODELS_URL = "/api/ollama/models";
name: string; const LOCAL_CHAT_URL = "/api/ollama/chat";
size?: number; const DEFAULT_MODELS = [
digest?: string; "gpt-oss:120b",
} "llama3.1:latest",
"llama3.1:70b",
"llama3.1:8b",
"llama3.1:instruct",
"gemma3:12b",
"gemma3:27b",
"gemma3:4b",
"gemma3:7b",
"deepseek-r1:70b",
"deepseek-r1:32b",
"deepseek-r1:14b",
"deepseek-r1:8b",
"deepseek-r1:1.5b",
"qwen3:72b",
"qwen3:32b",
"qwen3:14b",
"qwen3:7b",
"qwen3:4b",
"mistral:7b",
"mistral:instruct",
"codellama:34b",
"codellama:13b",
"codellama:7b",
"codellama:instruct",
"phi3:14b",
"phi3:3.8b",
"phi3:mini",
"gemma2:27b",
"gemma2:9b",
"yi:34b",
"yi:9b",
];
export class OllamaCloudService { export class OllamaCloudService {
private config: OllamaCloudConfig; private config: OllamaCloudConfig;
@@ -17,38 +48,50 @@ export class OllamaCloudService {
constructor(config: OllamaCloudConfig = {}) { constructor(config: OllamaCloudConfig = {}) {
this.config = { this.config = {
endpoint: config.endpoint || "https://ollama.com/api",
apiKey: config.apiKey || process.env.OLLAMA_API_KEY, apiKey: config.apiKey || process.env.OLLAMA_API_KEY,
endpoint: config.endpoint,
}; };
} }
private getHeaders(): Record<string, string> { hasAuth(): boolean {
return !!this.config.apiKey;
}
private ensureApiKey(): string {
if (this.config.apiKey) {
return this.config.apiKey;
}
throw new Error("API key is required. Please configure your Ollama API key in settings.");
}
private getHeaders(additional: Record<string, string> = {}) {
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/json", ...additional,
"x-ollama-api-key": this.ensureApiKey(),
}; };
if (this.config.apiKey) { if (this.config.endpoint) {
headers["Authorization"] = `Bearer ${this.config.apiKey}`; headers["x-ollama-endpoint"] = this.config.endpoint;
} }
return headers; return headers;
} }
private async parseJsonResponse(response: Response): Promise<any> {
const text = await response.text();
if (!text) return null;
return JSON.parse(text);
}
async chatCompletion( async chatCompletion(
messages: ChatMessage[], messages: ChatMessage[],
model: string = "gpt-oss:120b", model: string = "gpt-oss:120b",
stream: boolean = false stream: boolean = false
): Promise<APIResponse<string>> { ): Promise<APIResponse<string>> {
try { try {
if (!this.config.apiKey) { const response = await fetch(LOCAL_CHAT_URL, {
throw new Error("API key is required. Please configure your Ollama API key in settings.");
}
console.log("[Ollama] API call:", { endpoint: this.config.endpoint, model, messages });
const response = await fetch(`${this.config.endpoint}/chat`, {
method: "POST", method: "POST",
headers: this.getHeaders(), headers: this.getHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ body: JSON.stringify({
model, model,
messages, messages,
@@ -56,24 +99,23 @@ export class OllamaCloudService {
}), }),
}); });
console.log("[Ollama] Response status:", response.status, response.statusText);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorBody = await response.text();
console.error("[Ollama] Error response:", errorText); throw new Error(
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`); `Chat completion failed (${response.status}): ${response.statusText} - ${errorBody}`
);
} }
const data = await response.json(); const data = await this.parseJsonResponse(response);
console.log("[Ollama] Response data:", data); if (data?.message?.content) {
if (data.message && data.message.content) {
return { success: true, data: data.message.content }; return { success: true, data: data.message.content };
} else if (data.choices && data.choices[0]) {
return { success: true, data: data.choices[0].message.content };
} else {
return { success: false, error: "Unexpected response format" };
} }
if (data?.choices?.[0]?.message?.content) {
return { success: true, data: data.choices[0].message.content };
}
return { success: false, error: "Unexpected response format" };
} catch (error) { } catch (error) {
console.error("[Ollama] Chat completion error:", error); console.error("[Ollama] Chat completion error:", error);
return { return {
@@ -85,32 +127,31 @@ export class OllamaCloudService {
async listModels(): Promise<APIResponse<string[]>> { async listModels(): Promise<APIResponse<string[]>> {
try { try {
if (this.config.apiKey) { const response = await fetch(LOCAL_MODELS_URL, {
console.log("[Ollama] Listing models from:", `${this.config.endpoint}/tags`); headers: this.getHeaders(),
});
const response = await fetch(`${this.config.endpoint}/tags`, { if (!response.ok) {
headers: this.getHeaders(), const errorBody = await response.text();
}); throw new Error(`List models failed: ${response.statusText} - ${errorBody}`);
console.log("[Ollama] List models response status:", response.status, response.statusText);
if (!response.ok) {
throw new Error(`Failed to list models: ${response.statusText}`);
}
const data = await response.json();
console.log("[Ollama] Models data:", data);
const models = data.models?.map((m: OllamaModel) => m.name) || [];
this.availableModels = models;
return { success: true, data: models };
} else {
console.log("[Ollama] No API key, using fallback models");
return { success: true, data: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"] };
} }
const data = await this.parseJsonResponse(response);
const models: string[] = Array.isArray(data?.models) ? data.models : [];
if (models.length === 0) {
this.availableModels = DEFAULT_MODELS;
return { success: true, data: DEFAULT_MODELS };
}
this.availableModels = models;
return { success: true, data: models };
} catch (error) { } catch (error) {
console.error("[Ollama] listModels error:", error); console.error("[Ollama] listModels error:", error);
if (DEFAULT_MODELS.length > 0) {
this.availableModels = DEFAULT_MODELS;
return { success: true, data: DEFAULT_MODELS };
}
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : "Failed to list models", error: error instanceof Error ? error.message : "Failed to list models",
@@ -119,9 +160,7 @@ export class OllamaCloudService {
} }
getAvailableModels(): string[] { getAvailableModels(): string[] {
return this.availableModels.length > 0 return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS;
? this.availableModels
: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"];
} }
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> { async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
@@ -136,7 +175,7 @@ Apply these principles:
4. Include edge cases and error handling requirements 4. Include edge cases and error handling requirements
5. Specify testing and validation criteria 5. Specify testing and validation criteria
Return ONLY the enhanced prompt, no explanations.`, Return ONLY the enhanced prompt, no explanations or extra text.`,
}; };
const userMessage: ChatMessage = { const userMessage: ChatMessage = {
@@ -198,6 +237,71 @@ Include specific recommendations for:
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b"); return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
} }
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
const systemMessage: ChatMessage = {
role: "system",
content: `You are a world-class UX/UI designer with deep expertise in human-centered design principles, user research, interaction design, visual design systems, and modern design tools (Figma, Sketch, Adobe XD).
Your task is to create an exceptional, detailed prompt for generating best possible UX design for a given app description.
Generate a comprehensive UX design prompt that includes:
1. USER RESEARCH & PERSONAS
- Primary target users and their motivations
- User pain points and needs
- User journey maps
- Persona archetypes with demographics and goals
2. INFORMATION ARCHITECTURE
- Content hierarchy and organization
- Navigation structure and patterns
- User flows and key pathways
- Site map or app structure
3. VISUAL DESIGN SYSTEM
- Color palette recommendations (primary, secondary, accent, neutral)
- Typography hierarchy and font pairings
- Component library approach
- Spacing, sizing, and layout grids
- Iconography style and set
4. INTERACTION DESIGN
- Micro-interactions and animations
- Gesture patterns for touch interfaces
- Loading states and empty states
- Error handling and feedback mechanisms
- Accessibility considerations (WCAG compliance)
5. KEY SCREENS & COMPONENTS
- Core screens that need detailed design
- Critical components (buttons, forms, cards, navigation)
- Data visualization needs
- Responsive design requirements (mobile, tablet, desktop)
6. DESIGN DELIVERABLES
- Wireframes vs. high-fidelity mockups
- Design system documentation needs
- Prototyping requirements
- Handoff specifications for developers
7. COMPETITIVE INSIGHTS
- Design patterns from successful apps in this category
- Opportunities to differentiate
- Modern design trends to consider
The output should be a detailed, actionable prompt that a designer or AI image generator can use to create world-class UX designs.
Make's prompt specific, inspiring, and comprehensive. Use professional UX terminology.`,
};
const userMessage: ChatMessage = {
role: "user",
content: `Create a BEST EVER UX design prompt for this app:\n\n${appDescription}`,
};
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
}
} }
export default OllamaCloudService; export default OllamaCloudService;

View File

@@ -1,125 +1,481 @@
import type { ChatMessage, APIResponse } from "@/types"; 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 {
if (typeof window !== "undefined") {
return `${window.location.origin}/api/qwen`;
}
if (process.env.NEXT_PUBLIC_SITE_URL) {
return `${process.env.NEXT_PUBLIC_SITE_URL}/api/qwen`;
}
return "/api/qwen";
}
export interface QwenOAuthConfig { export interface QwenOAuthConfig {
apiKey?: string; apiKey?: string;
endpoint?: string;
oauthBaseUrl?: string;
accessToken?: string; accessToken?: string;
refreshToken?: string; refreshToken?: string;
expiresAt?: number; expiresAt?: number;
endpoint?: string; resourceUrl?: string;
clientId?: string; }
redirectUri?: 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 { 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 = {}) { constructor(config: QwenOAuthConfig = {}) {
this.config = { this.endpoint = config.endpoint || DEFAULT_QWEN_ENDPOINT;
endpoint: config.endpoint || "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", this.oauthBaseUrl = config.oauthBaseUrl || getOAuthBaseUrl();
apiKey: config.apiKey || process.env.QWEN_API_KEY, this.apiKey = config.apiKey || process.env.QWEN_API_KEY || undefined;
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 : ""),
};
}
private getHeaders(): Record<string, string> { if (config.accessToken) {
const authHeader = this.config.accessToken this.setOAuthTokens({
? `Bearer ${this.config.accessToken}` accessToken: config.accessToken,
: `Bearer ${this.config.apiKey}`; refreshToken: config.refreshToken,
expiresAt: config.expiresAt,
return { resourceUrl: config.resourceUrl,
"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",
};
} }
} }
setOAuthTokens(accessToken: string, refreshToken?: string, expiresIn?: number): void { /**
this.config.accessToken = accessToken; * Update the API key used for non-OAuth calls.
if (refreshToken) this.config.refreshToken = refreshToken; */
if (expiresIn) this.config.expiresAt = Date.now() + expiresIn * 1000; setApiKey(apiKey: string) {
this.apiKey = apiKey;
} }
getAuthorizationUrl(): string { hasApiKey(): boolean {
const baseUrl = "https://dashscope.console.aliyun.com/oauth/authorize"; // Placeholder URL return !!this.apiKey;
const params = new URLSearchParams({ }
client_id: this.config.clientId || "",
redirect_uri: this.config.redirectUri || "", hasOAuthToken(): boolean {
response_type: "code", return !!this.getTokenInfo()?.accessToken;
scope: "dashscope:chat", }
/**
* 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; * Returns a valid token, refreshing if necessary.
this.config.accessToken = undefined; */
this.config.refreshToken = undefined; private async getValidToken(): Promise<QwenOAuthToken | null> {
this.config.expiresAt = undefined; 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( async chatCompletion(
messages: ChatMessage[], messages: ChatMessage[],
model: string = "qwen-coder-plus", model: string = "coder-model",
stream: boolean = false stream: boolean = false
): Promise<APIResponse<string>> { ): Promise<APIResponse<string>> {
try { try {
if (!this.config.apiKey) { const headers = await this.getRequestHeaders();
throw new Error("API key is required. Please configure your Qwen API key in settings."); 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", method: "POST",
headers: this.getHeaders(), headers: {
"Content-Type": "application/json",
Authorization: headers.Authorization || "",
},
body: JSON.stringify({ body: JSON.stringify({
endpoint: baseUrl,
model, model,
messages, messages,
stream, stream,
}), }),
}); });
console.log("[Qwen] Response status:", response.status, response.statusText);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); 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}`); throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
} }
const data = await response.json(); const data = await response.json();
console.log("[Qwen] Response data:", data); if (data.choices?.[0]?.message) {
if (data.choices && data.choices[0] && data.choices[0].message) {
return { success: true, data: data.choices[0].message.content }; return { success: true, data: data.choices[0].message.content };
} else {
return { success: false, error: "Unexpected response format" };
} }
return { success: false, error: "Unexpected response format" };
} catch (error) { } catch (error) {
console.error("[Qwen] Chat completion error:", error); console.error("[Qwen] Chat completion error:", error);
return { return {
@@ -149,7 +505,7 @@ Return ONLY the enhanced prompt, no explanations or extra text.`,
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`, content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
}; };
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus"); return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
} }
async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> { async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> {
@@ -173,7 +529,7 @@ Use clear, specific language suitable for development teams.`,
content: `Generate a PRD for this idea:\n\n${idea}`, content: `Generate a PRD for this idea:\n\n${idea}`,
}; };
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus"); return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
} }
async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> { async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> {
@@ -201,17 +557,88 @@ Include specific recommendations for:
content: `Generate an action plan based on this PRD:\n\n${prd}`, content: `Generate an action plan based on this PRD:\n\n${prd}`,
}; };
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus"); return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
}
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
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 listModels(): Promise<APIResponse<string[]>> { 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 }; return { success: true, data: models };
} }
getAvailableModels(): string[] { 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 };

View File

@@ -17,6 +17,10 @@ export class ZaiPlanService {
}; };
} }
hasAuth(): boolean {
return !!this.config.apiKey;
}
private getHeaders(): Record<string, string> { private getHeaders(): Record<string, string> {
return { return {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -182,6 +186,71 @@ Include specific recommendations for:
getAvailableModels(): string[] { getAvailableModels(): string[] {
return ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"]; return ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"];
} }
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
const systemMessage: ChatMessage = {
role: "system",
content: `You are a world-class UX/UI designer with deep expertise in human-centered design principles, user research, interaction design, visual design systems, and modern design tools (Figma, Sketch, Adobe XD).
Your task is to create an exceptional, detailed prompt for generating the best possible UX design for a given app description.
Generate a comprehensive UX design prompt that includes:
1. USER RESEARCH & PERSONAS
- Primary target users and their motivations
- User pain points and needs
- User journey maps
- Persona archetypes with demographics and goals
2. INFORMATION ARCHITECTURE
- Content hierarchy and organization
- Navigation structure and patterns
- User flows and key pathways
- Site map or app structure
3. VISUAL DESIGN SYSTEM
- Color palette recommendations (primary, secondary, accent, neutral)
- Typography hierarchy and font pairings
- Component library approach
- Spacing, sizing, and layout grids
- Iconography style and set
4. INTERACTION DESIGN
- Micro-interactions and animations
- Gesture patterns for touch interfaces
- Loading states and empty states
- Error handling and feedback mechanisms
- Accessibility considerations (WCAG compliance)
5. KEY SCREENS & COMPONENTS
- Core screens that need detailed design
- Critical components (buttons, forms, cards, navigation)
- Data visualization needs
- Responsive design requirements (mobile, tablet, desktop)
6. DESIGN DELIVERABLES
- Wireframes vs. high-fidelity mockups
- Design system documentation needs
- Prototyping requirements
- Handoff specifications for developers
7. COMPETITIVE INSIGHTS
- Design patterns from successful apps in this category
- Opportunities to differentiate
- Modern design trends to consider
The output should be a detailed, actionable prompt that a designer or AI image generator can use to create world-class UX designs.
Make the prompt specific, inspiring, and comprehensive. Use professional UX terminology.`,
};
const userMessage: ChatMessage = {
role: "user",
content: `Create the BEST EVER UX design prompt for this app:\n\n${appDescription}`,
};
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
}
} }
export default ZaiPlanService; export default ZaiPlanService;

View File

@@ -14,7 +14,7 @@ interface AppState {
accessToken: string; accessToken: string;
refreshToken?: string; refreshToken?: string;
expiresAt?: number; expiresAt?: number;
}; } | null;
isProcessing: boolean; isProcessing: boolean;
error: string | null; error: string | null;
history: { history: {
@@ -31,7 +31,7 @@ interface AppState {
setSelectedModel: (provider: ModelProvider, model: string) => void; setSelectedModel: (provider: ModelProvider, model: string) => void;
setAvailableModels: (provider: ModelProvider, models: string[]) => void; setAvailableModels: (provider: ModelProvider, models: string[]) => void;
setApiKey: (provider: ModelProvider, key: string) => void; setApiKey: (provider: ModelProvider, key: string) => void;
setQwenTokens: (tokens: { accessToken: string; refreshToken?: string; expiresAt?: number }) => void; setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
setProcessing: (processing: boolean) => void; setProcessing: (processing: boolean) => void;
setError: (error: string | null) => void; setError: (error: string | null) => void;
addToHistory: (prompt: string) => void; addToHistory: (prompt: string) => void;
@@ -46,12 +46,12 @@ const useStore = create<AppState>((set) => ({
actionPlan: null, actionPlan: null,
selectedProvider: "qwen", selectedProvider: "qwen",
selectedModels: { selectedModels: {
qwen: "qwen-coder-plus", qwen: "coder-model",
ollama: "gpt-oss:120b", ollama: "gpt-oss:120b",
zai: "glm-4.7", zai: "glm-4.7",
}, },
availableModels: { availableModels: {
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"], qwen: ["coder-model"],
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"], ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"], zai: ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
}, },

379
package-lock.json generated
View File

@@ -17,7 +17,7 @@
"eslint": "^9.16.0", "eslint": "^9.16.0",
"eslint-config-next": "^15.0.3", "eslint-config-next": "^15.0.3",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^15.0.3", "next": "^16.1.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@@ -50,17 +50,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
@@ -71,16 +60,6 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -764,22 +743,10 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.5.9", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@@ -792,9 +759,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -808,9 +775,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -824,9 +791,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -840,9 +807,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -856,9 +823,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -872,9 +839,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -888,9 +855,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -904,9 +871,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.7", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1032,16 +999,6 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/d3-array": { "node_modules/@types/d3-array": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -1467,243 +1424,6 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
"integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@unrs/resolver-binding-android-arm64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
"integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@unrs/resolver-binding-darwin-arm64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
"integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@unrs/resolver-binding-darwin-x64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
"integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@unrs/resolver-binding-freebsd-x64": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
"integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
"integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
"integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
"integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-arm64-musl": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
"integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
"integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
"integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
"integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
"integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-x64-gnu": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
"integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-linux-x64-musl": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
"integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@unrs/resolver-binding-wasm32-wasi": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
"integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^0.2.11"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
"integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
"integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@unrs/resolver-binding-win32-x64-msvc": { "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
@@ -3499,20 +3219,6 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -5560,13 +5266,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/next": { "node_modules/next": {
"version": "15.5.9", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "15.5.9", "@next/env": "16.1.1",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31", "postcss": "8.4.31",
"styled-jsx": "5.1.6" "styled-jsx": "5.1.6"
@@ -5575,18 +5282,18 @@
"next": "dist/bin/next" "next": "dist/bin/next"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0" "node": ">=20.9.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.7", "@next/swc-darwin-arm64": "16.1.1",
"@next/swc-darwin-x64": "15.5.7", "@next/swc-darwin-x64": "16.1.1",
"@next/swc-linux-arm64-gnu": "15.5.7", "@next/swc-linux-arm64-gnu": "16.1.1",
"@next/swc-linux-arm64-musl": "15.5.7", "@next/swc-linux-arm64-musl": "16.1.1",
"@next/swc-linux-x64-gnu": "15.5.7", "@next/swc-linux-x64-gnu": "16.1.1",
"@next/swc-linux-x64-musl": "15.5.7", "@next/swc-linux-x64-musl": "16.1.1",
"@next/swc-win32-arm64-msvc": "15.5.7", "@next/swc-win32-arm64-msvc": "16.1.1",
"@next/swc-win32-x64-msvc": "15.5.7", "@next/swc-win32-x64-msvc": "16.1.1",
"sharp": "^0.34.3" "sharp": "^0.34.4"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",

View File

@@ -17,7 +17,7 @@
"eslint": "^9.16.0", "eslint": "^9.16.0",
"eslint-config-next": "^15.0.3", "eslint-config-next": "^15.0.3",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^15.0.3", "next": "^16.1.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@@ -37,7 +37,23 @@
"@types/react": "^19.0.1", "@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2" "@types/react-dom": "^19.0.2"
}, },
"keywords": [], "keywords": [
"author": "", "ai",
"license": "ISC" "prompt-engineering",
"prd-generator",
"nextjs",
"qwen",
"ollama",
"zai"
],
"author": "Roman | RyzenAdvanced <https://github.com/roman-ryzenadvanced>",
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git"
},
"bugs": {
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer/issues"
},
"homepage": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer#readme"
} }

View File

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

View File

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