Compare commits
4 Commits
remove-qwe
...
79204352fe
@@ -14,3 +14,7 @@ OLLAMA_ENDPOINT=https://ollama.com/api
|
||||
ZAI_API_KEY=
|
||||
ZAI_GENERAL_ENDPOINT=https://api.z.ai/api/paas/v4
|
||||
ZAI_CODING_ENDPOINT=https://api.z.ai/api/coding/paas/v4
|
||||
|
||||
# Site Configuration (Required for OAuth in production)
|
||||
# Set to your production URL (e.g., https://your-app.vercel.app)
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:6002
|
||||
|
||||
57
app/api/ollama/chat/route.ts
Normal file
57
app/api/ollama/chat/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { normalizeOllamaBase, DEFAULT_OLLAMA_BASE } from "../constants";
|
||||
|
||||
const API_PREFIX = "/api";
|
||||
|
||||
function getApiKey(request: NextRequest): string | null {
|
||||
return request.headers.get("x-ollama-api-key");
|
||||
}
|
||||
|
||||
function getBaseUrl(request: NextRequest): string {
|
||||
const header = request.headers.get("x-ollama-endpoint");
|
||||
if (header && header.trim().length > 0) {
|
||||
return normalizeOllamaBase(header);
|
||||
}
|
||||
return DEFAULT_OLLAMA_BASE;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const apiKey = getApiKey(request);
|
||||
if (!apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "Ollama API key is required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const baseUrl = getBaseUrl(request);
|
||||
const targetUrl = `${baseUrl}${API_PREFIX}/chat`;
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const payload = await response.text();
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Ollama chat request failed", details: payload },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(payload ? JSON.parse(payload) : {});
|
||||
} catch (error) {
|
||||
console.error("Ollama chat proxy failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Ollama chat request failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
7
app/api/ollama/constants.ts
Normal file
7
app/api/ollama/constants.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const DEFAULT_OLLAMA_BASE = process.env.NEXT_PUBLIC_OLLAMA_ENDPOINT || process.env.OLLAMA_ENDPOINT || "https://ollama.com";
|
||||
export function normalizeOllamaBase(url?: string): string {
|
||||
if (!url) return DEFAULT_OLLAMA_BASE.replace(/\/$/, "");
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) return DEFAULT_OLLAMA_BASE.replace(/\/$/, "");
|
||||
return trimmed.replace(/\/$/, "");
|
||||
}
|
||||
88
app/api/ollama/models/route.ts
Normal file
88
app/api/ollama/models/route.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { normalizeOllamaBase, DEFAULT_OLLAMA_BASE } from "../constants";
|
||||
|
||||
const API_PREFIX = "/api";
|
||||
|
||||
function getApiKey(request: NextRequest): string | null {
|
||||
return request.headers.get("x-ollama-api-key");
|
||||
}
|
||||
|
||||
function getBaseUrl(request: NextRequest): string {
|
||||
const header = request.headers.get("x-ollama-endpoint");
|
||||
if (header && header.trim().length > 0) {
|
||||
return normalizeOllamaBase(header);
|
||||
}
|
||||
return DEFAULT_OLLAMA_BASE;
|
||||
}
|
||||
|
||||
async function fetchModelNames(url: string, apiKey: string): Promise<string[]> {
|
||||
const response = await fetch(`${url}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "Failed to parse");
|
||||
throw new Error(`${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const json = await response.json().catch(() => null);
|
||||
const candidates = Array.isArray(json?.models)
|
||||
? json.models
|
||||
: Array.isArray(json?.data)
|
||||
? json.data
|
||||
: Array.isArray(json)
|
||||
? json
|
||||
: [];
|
||||
|
||||
const names: string[] = [];
|
||||
for (const entry of candidates) {
|
||||
if (!entry) continue;
|
||||
const name = entry.name || entry.model || entry.id;
|
||||
if (typeof name === "string" && name.length > 0) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const apiKey = getApiKey(request);
|
||||
if (!apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "Ollama API key is required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl = getBaseUrl(request);
|
||||
const primaryUrl = `${baseUrl}${API_PREFIX}/v1/models`;
|
||||
const fallbackUrl = `${baseUrl}${API_PREFIX}/tags`;
|
||||
|
||||
try {
|
||||
const primaryModels = await fetchModelNames(primaryUrl, apiKey);
|
||||
if (primaryModels.length > 0) {
|
||||
return NextResponse.json({ models: primaryModels });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Ollama] Primary model fetch failed:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
const fallbackModels = await fetchModelNames(fallbackUrl, apiKey);
|
||||
if (fallbackModels.length > 0) {
|
||||
return NextResponse.json({ models: fallbackModels });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Ollama] Fallback model fetch failed:", error);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ models: [] },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
6
app/api/qwen/constants.ts
Normal file
6
app/api/qwen/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
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";
|
||||
50
app/api/qwen/oauth/device/route.ts
Normal file
50
app/api/qwen/oauth/device/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
QWEN_OAUTH_CLIENT_ID,
|
||||
QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
|
||||
QWEN_OAUTH_SCOPE,
|
||||
} 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 }
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: QWEN_OAUTH_CLIENT_ID,
|
||||
scope: QWEN_OAUTH_SCOPE,
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.text();
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Device authorization failed", details: payload },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(payload));
|
||||
} catch (error) {
|
||||
console.error("Qwen device authorization failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Device authorization failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
45
app/api/qwen/oauth/refresh/route.ts
Normal file
45
app/api/qwen/oauth/refresh/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { QWEN_OAUTH_CLIENT_ID, QWEN_OAUTH_TOKEN_ENDPOINT } 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: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token,
|
||||
client_id: QWEN_OAUTH_CLIENT_ID,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.text();
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Token refresh failed", details: payload },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(payload));
|
||||
} catch (error) {
|
||||
console.error("Qwen token refresh failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Token refresh failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
50
app/api/qwen/oauth/token/route.ts
Normal file
50
app/api/qwen/oauth/token/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
QWEN_OAUTH_CLIENT_ID,
|
||||
QWEN_OAUTH_DEVICE_GRANT_TYPE,
|
||||
QWEN_OAUTH_TOKEN_ENDPOINT,
|
||||
} 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 }
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
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();
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Token poll failed", details: payload },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(payload));
|
||||
} catch (error) {
|
||||
console.error("Qwen token poll failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Token poll failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
38
app/api/qwen/user/route.ts
Normal file
38
app/api/qwen/user/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { QWEN_OAUTH_BASE_URL } 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: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userResponse.ok) {
|
||||
const errorText = await userResponse.text();
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch user info", details: errorText },
|
||||
{ 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: "Failed to fetch user info" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
55
app/page.tsx
55
app/page.tsx
@@ -1,66 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import type { View } from "@/components/Sidebar";
|
||||
import PromptEnhancer from "@/components/PromptEnhancer";
|
||||
import PRDGenerator from "@/components/PRDGenerator";
|
||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
||||
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
||||
import HistoryPanel from "@/components/HistoryPanel";
|
||||
import SettingsPanel from "@/components/SettingsPanel";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
|
||||
export default function Home() {
|
||||
const [currentView, setCurrentView] = useState<View>("enhance");
|
||||
const { setQwenTokens, setApiKey } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Handle OAuth callback
|
||||
if (typeof window !== "undefined") {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get("code");
|
||||
|
||||
if (code) {
|
||||
// In a real app, you would exchange the code for tokens here
|
||||
// Since we don't have a backend or real client secret, we'll simulate it
|
||||
console.log("OAuth code received:", code);
|
||||
|
||||
// Mock token exchange
|
||||
const mockAccessToken = "mock_access_token_" + Math.random().toString(36).substr(2, 9);
|
||||
const tokens = {
|
||||
accessToken: mockAccessToken,
|
||||
expiresAt: Date.now() + 3600 * 1000, // 1 hour
|
||||
};
|
||||
|
||||
setQwenTokens(tokens);
|
||||
modelAdapter.setQwenOAuthTokens(tokens.accessToken, undefined, 3600);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem("promptarch-qwen-tokens", JSON.stringify(tokens));
|
||||
|
||||
// Clear the code from URL
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
|
||||
// Switch to settings to show success (optional)
|
||||
setCurrentView("settings");
|
||||
}
|
||||
|
||||
// Load tokens from localStorage on init
|
||||
const savedTokens = localStorage.getItem("promptarch-qwen-tokens");
|
||||
if (savedTokens) {
|
||||
try {
|
||||
const tokens = JSON.parse(savedTokens);
|
||||
if (tokens.expiresAt > Date.now()) {
|
||||
setQwenTokens(tokens);
|
||||
modelAdapter.setQwenOAuthTokens(tokens.accessToken, tokens.refreshToken, (tokens.expiresAt - Date.now()) / 1000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load Qwen tokens:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (currentView) {
|
||||
@@ -70,6 +21,8 @@ export default function Home() {
|
||||
return <PRDGenerator />;
|
||||
case "action":
|
||||
return <ActionPlanGenerator />;
|
||||
case "uxdesigner":
|
||||
return <UXDesignerPrompt />;
|
||||
case "history":
|
||||
return <HistoryPanel />;
|
||||
case "settings":
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -43,6 +42,10 @@ export default function SettingsPanel() {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
}
|
||||
const storedTokens = modelAdapter.getQwenTokenInfo();
|
||||
if (storedTokens) {
|
||||
setQwenTokens(storedTokens);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,6 +65,28 @@ 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);
|
||||
} catch (error) {
|
||||
console.error("Qwen OAuth failed", error);
|
||||
window.alert(
|
||||
error instanceof Error ? error.message : "Qwen authentication failed"
|
||||
);
|
||||
} finally {
|
||||
setIsAuthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad();
|
||||
}, []);
|
||||
@@ -122,17 +147,14 @@ export default function SettingsPanel() {
|
||||
variant={qwenTokens ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => {
|
||||
if (qwenTokens) {
|
||||
setQwenTokens(undefined as any);
|
||||
localStorage.removeItem("promptarch-qwen-tokens");
|
||||
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
|
||||
} else {
|
||||
window.location.href = modelAdapter.getQwenAuthUrl();
|
||||
}
|
||||
}}
|
||||
onClick={handleQwenAuth}
|
||||
disabled={isAuthLoading}
|
||||
>
|
||||
{qwenTokens ? "Logout from Qwen" : "Login with Qwen (OAuth)"}
|
||||
{isAuthLoading
|
||||
? "Signing in..."
|
||||
: qwenTokens
|
||||
? "Logout from Qwen"
|
||||
: "Login with Qwen (OAuth)"}
|
||||
</Button>
|
||||
</div>
|
||||
{qwenTokens && (
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useStore from "@/lib/store";
|
||||
import { Sparkles, FileText, ListTodo, Settings, History } from "lucide-react";
|
||||
import { Sparkles, FileText, ListTodo, Palette, History, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type View = "enhance" | "prd" | "action" | "history" | "settings";
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "history" | "settings";
|
||||
|
||||
interface SidebarProps {
|
||||
currentView: View;
|
||||
@@ -19,6 +19,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
|
||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
||||
{ id: "uxdesigner" as View, label: "UX Designer Prompt", icon: Palette },
|
||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
||||
];
|
||||
@@ -81,6 +82,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
<li>• Use different providers for best results</li>
|
||||
<li>• Copy enhanced prompts to your AI agent</li>
|
||||
<li>• PRDs generate better action plans</li>
|
||||
<li>• UX Designer Prompt for design tasks</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
242
components/UXDesignerPrompt.tsx
Normal file
242
components/UXDesignerPrompt.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
"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];
|
||||
if (!apiKey || !apiKey.trim()) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
setGeneratedPrompt(null);
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setGeneratedPrompt(result.data);
|
||||
setEnhancedPrompt(result.data);
|
||||
} else {
|
||||
setError(result.error || "Failed to generate UX designer prompt");
|
||||
}
|
||||
} catch (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-6 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Palette className="h-5 w-5" />
|
||||
UX Designer Prompt
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Describe your app idea and get the BEST EVER prompt for UX design
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={cn(
|
||||
"capitalize",
|
||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
>
|
||||
{provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="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-[200px] resize-y"
|
||||
/>
|
||||
<p className="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-3 text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Palette className="mr-2 h-4 w-4" />
|
||||
Generate UX Prompt
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing}>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!generatedPrompt && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
Best Ever UX Prompt
|
||||
</span>
|
||||
{generatedPrompt && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Comprehensive UX design prompt ready for designers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{generatedPrompt ? (
|
||||
<div className="rounded-md border bg-muted/50 p-4">
|
||||
<pre className="whitespace-pre-wrap text-sm">{generatedPrompt}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[400px] items-center justify-center text-center text-sm text-muted-foreground">
|
||||
Your comprehensive UX designer prompt will appear here
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
|
||||
import QwenOAuthService from "./qwen-oauth";
|
||||
import OllamaCloudService from "./ollama-cloud";
|
||||
import ZaiPlanService from "./zai-plan";
|
||||
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
|
||||
|
||||
export interface ModelAdapterConfig {
|
||||
qwen?: {
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
};
|
||||
qwen?: QwenOAuthConfig;
|
||||
ollama?: {
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
@@ -20,34 +17,35 @@ export interface ModelAdapterConfig {
|
||||
}
|
||||
|
||||
export class ModelAdapter {
|
||||
private qwenService: QwenOAuthService;
|
||||
private ollamaService: OllamaCloudService;
|
||||
private zaiService: ZaiPlanService;
|
||||
private qwenService = qwenOAuthService;
|
||||
private preferredProvider: ModelProvider;
|
||||
|
||||
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "qwen") {
|
||||
this.qwenService = new QwenOAuthService(config.qwen);
|
||||
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "ollama") {
|
||||
this.ollamaService = new OllamaCloudService(config.ollama);
|
||||
this.zaiService = new ZaiPlanService(config.zai);
|
||||
this.preferredProvider = preferredProvider;
|
||||
|
||||
if (config.qwen) {
|
||||
if (config.qwen.apiKey) {
|
||||
this.qwenService.setApiKey(config.qwen.apiKey);
|
||||
}
|
||||
if (config.qwen.accessToken) {
|
||||
this.qwenService.setOAuthTokens({
|
||||
accessToken: config.qwen.accessToken,
|
||||
refreshToken: config.qwen.refreshToken,
|
||||
expiresAt: config.qwen.expiresAt,
|
||||
resourceUrl: config.qwen.resourceUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPreferredProvider(provider: ModelProvider): void {
|
||||
this.preferredProvider = provider;
|
||||
}
|
||||
|
||||
updateQwenApiKey(apiKey: string): void {
|
||||
this.qwenService = new QwenOAuthService({ apiKey });
|
||||
}
|
||||
|
||||
setQwenOAuthTokens(accessToken: string, refreshToken?: string, expiresIn?: number): void {
|
||||
this.qwenService.setOAuthTokens(accessToken, refreshToken, expiresIn);
|
||||
}
|
||||
|
||||
getQwenAuthUrl(): string {
|
||||
return this.qwenService.getAuthorizationUrl();
|
||||
}
|
||||
|
||||
updateOllamaApiKey(apiKey: string): void {
|
||||
this.ollamaService = new OllamaCloudService({ apiKey });
|
||||
}
|
||||
@@ -56,6 +54,33 @@ export class ModelAdapter {
|
||||
this.zaiService = new ZaiPlanService({ apiKey });
|
||||
}
|
||||
|
||||
updateQwenApiKey(apiKey: string): void {
|
||||
this.qwenService.setApiKey(apiKey);
|
||||
}
|
||||
|
||||
updateQwenTokens(tokens?: QwenOAuthToken): void {
|
||||
this.qwenService.setOAuthTokens(tokens);
|
||||
}
|
||||
|
||||
async startQwenOAuth(): Promise<QwenOAuthToken> {
|
||||
return await this.qwenService.signIn();
|
||||
}
|
||||
|
||||
getQwenTokenInfo(): QwenOAuthToken | null {
|
||||
return this.qwenService.getTokenInfo();
|
||||
}
|
||||
|
||||
private buildFallbackProviders(...providers: ModelProvider[]): ModelProvider[] {
|
||||
const seen = new Set<ModelProvider>();
|
||||
return providers.filter((provider) => {
|
||||
if (seen.has(provider)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(provider);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private async callWithFallback<T>(
|
||||
operation: (service: any) => Promise<APIResponse<T>>,
|
||||
providers: ModelProvider[]
|
||||
@@ -92,20 +117,29 @@ export class ModelAdapter {
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const providers: ModelProvider[] = provider ? [provider] : [this.preferredProvider, "ollama", "zai"];
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.enhancePrompt(prompt, model), providers);
|
||||
}
|
||||
|
||||
async generatePRD(idea: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const providers: ModelProvider[] = provider ? [provider] : ["ollama", "zai", this.preferredProvider];
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generatePRD(idea, model), providers);
|
||||
}
|
||||
|
||||
async generateActionPlan(prd: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const providers: ModelProvider[] = provider ? [provider] : ["zai", "ollama", this.preferredProvider];
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateActionPlan(prd, model), providers);
|
||||
}
|
||||
|
||||
async generateUXDesignerPrompt(appDescription: string, provider?: ModelProvider, model?: string): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers);
|
||||
}
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string,
|
||||
@@ -137,7 +171,7 @@ export class ModelAdapter {
|
||||
|
||||
async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> {
|
||||
const fallbackModels: Record<ModelProvider, string[]> = {
|
||||
qwen: ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite"],
|
||||
qwen: this.qwenService.getAvailableModels(),
|
||||
ollama: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"],
|
||||
zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||
};
|
||||
@@ -163,16 +197,6 @@ export class ModelAdapter {
|
||||
console.error("[ModelAdapter] Failed to load Z.AI models, using fallback:", error);
|
||||
}
|
||||
}
|
||||
if (provider === "qwen" || !provider) {
|
||||
try {
|
||||
const qwenModels = await this.qwenService.listModels();
|
||||
if (qwenModels.success && qwenModels.data && qwenModels.data.length > 0) {
|
||||
models.qwen = qwenModels.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[ModelAdapter] Failed to load Qwen models, using fallback:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: models };
|
||||
}
|
||||
|
||||
@@ -5,11 +5,42 @@ export interface OllamaCloudConfig {
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export interface OllamaModel {
|
||||
name: string;
|
||||
size?: number;
|
||||
digest?: string;
|
||||
}
|
||||
const LOCAL_MODELS_URL = "/api/ollama/models";
|
||||
const LOCAL_CHAT_URL = "/api/ollama/chat";
|
||||
const DEFAULT_MODELS = [
|
||||
"gpt-oss:120b",
|
||||
"llama3.1:latest",
|
||||
"llama3.1:70b",
|
||||
"llama3.1:8b",
|
||||
"llama3.1:instruct",
|
||||
"gemma3:12b",
|
||||
"gemma3:27b",
|
||||
"gemma3:4b",
|
||||
"gemma3:7b",
|
||||
"deepseek-r1:70b",
|
||||
"deepseek-r1:32b",
|
||||
"deepseek-r1:14b",
|
||||
"deepseek-r1:8b",
|
||||
"deepseek-r1:1.5b",
|
||||
"qwen3:72b",
|
||||
"qwen3:32b",
|
||||
"qwen3:14b",
|
||||
"qwen3:7b",
|
||||
"qwen3:4b",
|
||||
"mistral:7b",
|
||||
"mistral:instruct",
|
||||
"codellama:34b",
|
||||
"codellama:13b",
|
||||
"codellama:7b",
|
||||
"codellama:instruct",
|
||||
"phi3:14b",
|
||||
"phi3:3.8b",
|
||||
"phi3:mini",
|
||||
"gemma2:27b",
|
||||
"gemma2:9b",
|
||||
"yi:34b",
|
||||
"yi:9b",
|
||||
];
|
||||
|
||||
export class OllamaCloudService {
|
||||
private config: OllamaCloudConfig;
|
||||
@@ -17,38 +48,46 @@ export class OllamaCloudService {
|
||||
|
||||
constructor(config: OllamaCloudConfig = {}) {
|
||||
this.config = {
|
||||
endpoint: config.endpoint || "https://ollama.com/api",
|
||||
apiKey: config.apiKey || process.env.OLLAMA_API_KEY,
|
||||
endpoint: config.endpoint,
|
||||
};
|
||||
}
|
||||
|
||||
private getHeaders(): Record<string, string> {
|
||||
private ensureApiKey(): string {
|
||||
if (this.config.apiKey) {
|
||||
return this.config.apiKey;
|
||||
}
|
||||
throw new Error("API key is required. Please configure your Ollama API key in settings.");
|
||||
}
|
||||
|
||||
private getHeaders(additional: Record<string, string> = {}) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...additional,
|
||||
"x-ollama-api-key": this.ensureApiKey(),
|
||||
};
|
||||
|
||||
if (this.config.apiKey) {
|
||||
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
||||
if (this.config.endpoint) {
|
||||
headers["x-ollama-endpoint"] = this.config.endpoint;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private async parseJsonResponse(response: Response): Promise<any> {
|
||||
const text = await response.text();
|
||||
if (!text) return null;
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string = "gpt-oss:120b",
|
||||
stream: boolean = false
|
||||
): Promise<APIResponse<string>> {
|
||||
try {
|
||||
if (!this.config.apiKey) {
|
||||
throw new Error("API key is required. Please configure your Ollama API key in settings.");
|
||||
}
|
||||
|
||||
console.log("[Ollama] API call:", { endpoint: this.config.endpoint, model, messages });
|
||||
|
||||
const response = await fetch(`${this.config.endpoint}/chat`, {
|
||||
const response = await fetch(LOCAL_CHAT_URL, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
headers: this.getHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages,
|
||||
@@ -56,24 +95,23 @@ export class OllamaCloudService {
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("[Ollama] Response status:", response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[Ollama] Error response:", errorText);
|
||||
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
|
||||
const errorBody = await response.text();
|
||||
throw new Error(
|
||||
`Chat completion failed (${response.status}): ${response.statusText} - ${errorBody}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Ollama] Response data:", data);
|
||||
|
||||
if (data.message && data.message.content) {
|
||||
const data = await this.parseJsonResponse(response);
|
||||
if (data?.message?.content) {
|
||||
return { success: true, data: data.message.content };
|
||||
} else if (data.choices && data.choices[0]) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
} else {
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
}
|
||||
|
||||
if (data?.choices?.[0]?.message?.content) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
}
|
||||
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
} catch (error) {
|
||||
console.error("[Ollama] Chat completion error:", error);
|
||||
return {
|
||||
@@ -85,32 +123,31 @@ export class OllamaCloudService {
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
try {
|
||||
if (this.config.apiKey) {
|
||||
console.log("[Ollama] Listing models from:", `${this.config.endpoint}/tags`);
|
||||
const response = await fetch(LOCAL_MODELS_URL, {
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.config.endpoint}/tags`, {
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
console.log("[Ollama] List models response status:", response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list models: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Ollama] Models data:", data);
|
||||
const models = data.models?.map((m: OllamaModel) => m.name) || [];
|
||||
|
||||
this.availableModels = models;
|
||||
|
||||
return { success: true, data: models };
|
||||
} else {
|
||||
console.log("[Ollama] No API key, using fallback models");
|
||||
return { success: true, data: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"] };
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(`List models failed: ${response.statusText} - ${errorBody}`);
|
||||
}
|
||||
|
||||
const data = await this.parseJsonResponse(response);
|
||||
const models: string[] = Array.isArray(data?.models) ? data.models : [];
|
||||
|
||||
if (models.length === 0) {
|
||||
this.availableModels = DEFAULT_MODELS;
|
||||
return { success: true, data: DEFAULT_MODELS };
|
||||
}
|
||||
|
||||
this.availableModels = models;
|
||||
return { success: true, data: models };
|
||||
} catch (error) {
|
||||
console.error("[Ollama] listModels error:", error);
|
||||
if (DEFAULT_MODELS.length > 0) {
|
||||
this.availableModels = DEFAULT_MODELS;
|
||||
return { success: true, data: DEFAULT_MODELS };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to list models",
|
||||
@@ -119,84 +156,7 @@ export class OllamaCloudService {
|
||||
}
|
||||
|
||||
getAvailableModels(): string[] {
|
||||
return this.availableModels.length > 0
|
||||
? this.availableModels
|
||||
: ["gpt-oss:120b", "llama3.1", "gemma3", "deepseek-r1", "qwen3"];
|
||||
}
|
||||
|
||||
async enhancePrompt(prompt: string, model?: string): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an expert prompt engineer. Your task is to enhance user prompts to make them more precise, actionable, and effective for AI coding agents.
|
||||
|
||||
Apply these principles:
|
||||
1. Add specific context about project and requirements
|
||||
2. Clarify constraints and preferences
|
||||
3. Define expected output format clearly
|
||||
4. Include edge cases and error handling requirements
|
||||
5. Specify testing and validation criteria
|
||||
|
||||
Return ONLY the enhanced prompt, no explanations.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Enhance this prompt for an AI coding agent:\n\n${prompt}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generatePRD(idea: string, model?: string): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an expert product manager and technical architect. Generate a comprehensive Product Requirements Document (PRD) based on user's idea.
|
||||
|
||||
Structure your PRD with these sections:
|
||||
1. Overview & Objectives
|
||||
2. User Personas & Use Cases
|
||||
3. Functional Requirements (prioritized)
|
||||
4. Non-functional Requirements
|
||||
5. Technical Architecture Recommendations
|
||||
6. Success Metrics & KPIs
|
||||
|
||||
Use clear, specific language suitable for development teams.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Generate a PRD for this idea:\n\n${idea}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateActionPlan(prd: string, model?: string): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an expert technical lead and project manager. Generate a detailed action plan based on PRD.
|
||||
|
||||
Structure of action plan with:
|
||||
1. Task breakdown with priorities (High/Medium/Low)
|
||||
2. Dependencies between tasks
|
||||
3. Estimated effort for each task
|
||||
4. Recommended frameworks and technologies
|
||||
5. Architecture guidelines and best practices
|
||||
|
||||
Include specific recommendations for:
|
||||
- Frontend frameworks
|
||||
- Backend architecture
|
||||
- Database choices
|
||||
- Authentication/authorization
|
||||
- Deployment strategy`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Generate an action plan based on this PRD:\n\n${prd}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
return this.availableModels.length > 0 ? this.availableModels : DEFAULT_MODELS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +1,391 @@
|
||||
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 {
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
oauthBaseUrl?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
endpoint?: string;
|
||||
clientId?: string;
|
||||
redirectUri?: string;
|
||||
resourceUrl?: string;
|
||||
}
|
||||
|
||||
export interface QwenOAuthToken {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
resourceUrl?: string;
|
||||
}
|
||||
|
||||
export interface QwenDeviceAuthorization {
|
||||
device_code: string;
|
||||
user_code: string;
|
||||
verification_uri: string;
|
||||
verification_uri_complete: string;
|
||||
expires_in: number;
|
||||
interval?: number;
|
||||
}
|
||||
|
||||
export class QwenOAuthService {
|
||||
private config: QwenOAuthConfig;
|
||||
private endpoint: string;
|
||||
private oauthBaseUrl: string;
|
||||
private apiKey?: string;
|
||||
private token: QwenOAuthToken | null = null;
|
||||
private storageHydrated = false;
|
||||
|
||||
constructor(config: QwenOAuthConfig = {}) {
|
||||
this.config = {
|
||||
endpoint: config.endpoint || "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: config.apiKey || process.env.QWEN_API_KEY,
|
||||
accessToken: config.accessToken,
|
||||
refreshToken: config.refreshToken,
|
||||
expiresAt: config.expiresAt,
|
||||
clientId: config.clientId || process.env.NEXT_PUBLIC_QWEN_CLIENT_ID,
|
||||
redirectUri: config.redirectUri || (typeof window !== "undefined" ? window.location.origin : ""),
|
||||
};
|
||||
}
|
||||
this.endpoint = config.endpoint || DEFAULT_QWEN_ENDPOINT;
|
||||
this.oauthBaseUrl = config.oauthBaseUrl || getOAuthBaseUrl();
|
||||
this.apiKey = config.apiKey || process.env.QWEN_API_KEY || undefined;
|
||||
|
||||
private getHeaders(): Record<string, string> {
|
||||
const authHeader = this.config.accessToken
|
||||
? `Bearer ${this.config.accessToken}`
|
||||
: `Bearer ${this.config.apiKey}`;
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": authHeader,
|
||||
};
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!(this.config.apiKey || (this.config.accessToken && (!this.config.expiresAt || this.config.expiresAt > Date.now())));
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
return this.config.accessToken || this.config.apiKey || null;
|
||||
}
|
||||
|
||||
async authenticate(apiKey: string): Promise<APIResponse<string>> {
|
||||
try {
|
||||
this.config.apiKey = apiKey;
|
||||
this.config.accessToken = undefined; // Clear OAuth token if API key is provided
|
||||
return { success: true, data: "Authenticated successfully" };
|
||||
} catch (error) {
|
||||
console.error("Qwen authentication error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Authentication failed",
|
||||
};
|
||||
if (config.accessToken) {
|
||||
this.setOAuthTokens({
|
||||
accessToken: config.accessToken,
|
||||
refreshToken: config.refreshToken,
|
||||
expiresAt: config.expiresAt,
|
||||
resourceUrl: config.resourceUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setOAuthTokens(accessToken: string, refreshToken?: string, expiresIn?: number): void {
|
||||
this.config.accessToken = accessToken;
|
||||
if (refreshToken) this.config.refreshToken = refreshToken;
|
||||
if (expiresIn) this.config.expiresAt = Date.now() + expiresIn * 1000;
|
||||
/**
|
||||
* Update the API key used for non-OAuth calls.
|
||||
*/
|
||||
setApiKey(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
getAuthorizationUrl(): string {
|
||||
const baseUrl = "https://dashscope.console.aliyun.com/oauth/authorize"; // Placeholder URL
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.config.clientId || "",
|
||||
redirect_uri: this.config.redirectUri || "",
|
||||
response_type: "code",
|
||||
scope: "dashscope:chat",
|
||||
/**
|
||||
* Build default headers for Qwen completions (includes OAuth token refresh).
|
||||
*/
|
||||
private async getRequestHeaders(): Promise<Record<string, string>> {
|
||||
const token = await this.getValidToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (token?.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${token.accessToken}`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
if (this.apiKey) {
|
||||
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
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) {
|
||||
return this.normalizeResourceUrl(resourceUrl);
|
||||
}
|
||||
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(/\/$/, "");
|
||||
return cleaned.endsWith("/v1") ? cleaned : `${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();
|
||||
return this.token;
|
||||
}
|
||||
|
||||
private persistToken(token: QwenOAuthToken | null) {
|
||||
if (typeof window === "undefined" || typeof window.localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (token) {
|
||||
window.localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
||||
} else {
|
||||
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[QwenOAuth] Failed to persist tokens to localStorage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private isTokenExpired(token: QwenOAuthToken): boolean {
|
||||
if (!token.expiresAt) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() >= token.expiresAt - 60_000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the OAuth token using the stored refresh token.
|
||||
*/
|
||||
private async refreshToken(refreshToken: string): Promise<QwenOAuthToken> {
|
||||
const response = await fetch(`${this.oauthBaseUrl}/oauth/refresh`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || "Failed to refresh Qwen token");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return this.parseTokenResponse(data);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.config.apiKey = undefined;
|
||||
this.config.accessToken = undefined;
|
||||
this.config.refreshToken = undefined;
|
||||
this.config.expiresAt = undefined;
|
||||
/**
|
||||
* Returns a valid token, refreshing if necessary.
|
||||
*/
|
||||
private async getValidToken(): Promise<QwenOAuthToken | null> {
|
||||
const token = this.getStoredToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.isTokenExpired(token)) {
|
||||
if (token.refreshToken) {
|
||||
try {
|
||||
const refreshed = await this.refreshToken(token.refreshToken);
|
||||
this.setOAuthTokens(refreshed);
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
console.error("Qwen token refresh failed", error);
|
||||
this.setOAuthTokens(undefined);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
this.setOAuthTokens(undefined);
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out the OAuth session.
|
||||
*/
|
||||
signOut(): void {
|
||||
this.setOAuthTokens(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores OAuth tokens locally.
|
||||
*/
|
||||
setOAuthTokens(tokens?: QwenOAuthToken | null) {
|
||||
if (!tokens) {
|
||||
this.token = null;
|
||||
this.persistToken(null);
|
||||
this.storageHydrated = true;
|
||||
return;
|
||||
}
|
||||
this.token = tokens;
|
||||
this.persistToken(tokens);
|
||||
this.storageHydrated = true;
|
||||
}
|
||||
|
||||
getTokenInfo(): QwenOAuthToken | null {
|
||||
return this.getStoredToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
resourceUrl: data.resource_url,
|
||||
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
@@ -88,15 +394,12 @@ export class QwenOAuthService {
|
||||
stream: boolean = false
|
||||
): Promise<APIResponse<string>> {
|
||||
try {
|
||||
if (!this.config.apiKey) {
|
||||
throw new Error("API key is required. Please configure your Qwen API key in settings.");
|
||||
}
|
||||
const headers = await this.getRequestHeaders();
|
||||
const url = `${this.getEffectiveEndpoint()}/chat/completions`;
|
||||
|
||||
console.log("[Qwen] API call:", { endpoint: this.config.endpoint, model, messages });
|
||||
|
||||
const response = await fetch(`${this.config.endpoint}/chat/completions`, {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages,
|
||||
@@ -104,22 +407,17 @@ export class QwenOAuthService {
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("[Qwen] Response status:", response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[Qwen] Error response:", errorText);
|
||||
throw new Error(`Chat completion failed (${response.status}): ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Qwen] Response data:", data);
|
||||
|
||||
if (data.choices && data.choices[0] && data.choices[0].message) {
|
||||
if (data.choices?.[0]?.message) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
} else {
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
}
|
||||
|
||||
return { success: false, error: "Unexpected response format" };
|
||||
} catch (error) {
|
||||
console.error("[Qwen] Chat completion error:", error);
|
||||
return {
|
||||
@@ -204,14 +502,95 @@ Include specific recommendations for:
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus");
|
||||
}
|
||||
|
||||
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 || "qwen-coder-plus");
|
||||
}
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
const models = ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite", "qwen-plus", "qwen-turbo", "qwen-max"];
|
||||
const models = [
|
||||
"qwen-coder-plus",
|
||||
"qwen-coder-turbo",
|
||||
"qwen-coder-lite",
|
||||
"qwen-plus",
|
||||
"qwen-turbo",
|
||||
"qwen-max",
|
||||
];
|
||||
return { success: true, data: models };
|
||||
}
|
||||
|
||||
getAvailableModels(): string[] {
|
||||
return ["qwen-coder-plus", "qwen-coder-turbo", "qwen-coder-lite", "qwen-plus", "qwen-turbo", "qwen-max"];
|
||||
return [
|
||||
"qwen-coder-plus",
|
||||
"qwen-coder-turbo",
|
||||
"qwen-coder-lite",
|
||||
"qwen-plus",
|
||||
"qwen-turbo",
|
||||
"qwen-max",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export default QwenOAuthService;
|
||||
const qwenOAuthService = new QwenOAuthService();
|
||||
export default qwenOAuthService;
|
||||
export { qwenOAuthService };
|
||||
|
||||
@@ -182,6 +182,71 @@ Include specific recommendations for:
|
||||
getAvailableModels(): string[] {
|
||||
return ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"];
|
||||
}
|
||||
|
||||
async generateUXDesignerPrompt(appDescription: string, model?: string): Promise<APIResponse<string>> {
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a world-class UX/UI designer with deep expertise in human-centered design principles, user research, interaction design, visual design systems, and modern design tools (Figma, Sketch, Adobe XD).
|
||||
|
||||
Your task is to create an exceptional, detailed prompt for generating the best possible UX design for a given app description.
|
||||
|
||||
Generate a comprehensive UX design prompt that includes:
|
||||
|
||||
1. USER RESEARCH & PERSONAS
|
||||
- Primary target users and their motivations
|
||||
- User pain points and needs
|
||||
- User journey maps
|
||||
- Persona archetypes with demographics and goals
|
||||
|
||||
2. INFORMATION ARCHITECTURE
|
||||
- Content hierarchy and organization
|
||||
- Navigation structure and patterns
|
||||
- User flows and key pathways
|
||||
- Site map or app structure
|
||||
|
||||
3. VISUAL DESIGN SYSTEM
|
||||
- Color palette recommendations (primary, secondary, accent, neutral)
|
||||
- Typography hierarchy and font pairings
|
||||
- Component library approach
|
||||
- Spacing, sizing, and layout grids
|
||||
- Iconography style and set
|
||||
|
||||
4. INTERACTION DESIGN
|
||||
- Micro-interactions and animations
|
||||
- Gesture patterns for touch interfaces
|
||||
- Loading states and empty states
|
||||
- Error handling and feedback mechanisms
|
||||
- Accessibility considerations (WCAG compliance)
|
||||
|
||||
5. KEY SCREENS & COMPONENTS
|
||||
- Core screens that need detailed design
|
||||
- Critical components (buttons, forms, cards, navigation)
|
||||
- Data visualization needs
|
||||
- Responsive design requirements (mobile, tablet, desktop)
|
||||
|
||||
6. DESIGN DELIVERABLES
|
||||
- Wireframes vs. high-fidelity mockups
|
||||
- Design system documentation needs
|
||||
- Prototyping requirements
|
||||
- Handoff specifications for developers
|
||||
|
||||
7. COMPETITIVE INSIGHTS
|
||||
- Design patterns from successful apps in this category
|
||||
- Opportunities to differentiate
|
||||
- Modern design trends to consider
|
||||
|
||||
The output should be a detailed, actionable prompt that a designer or AI image generator can use to create world-class UX designs.
|
||||
|
||||
Make the prompt specific, inspiring, and comprehensive. Use professional UX terminology.`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create the BEST EVER UX design prompt for this app:\n\n${appDescription}`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
}
|
||||
|
||||
export default ZaiPlanService;
|
||||
|
||||
@@ -14,7 +14,7 @@ interface AppState {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: number;
|
||||
};
|
||||
} | null;
|
||||
isProcessing: boolean;
|
||||
error: string | null;
|
||||
history: {
|
||||
@@ -31,7 +31,7 @@ interface AppState {
|
||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||
setApiKey: (provider: ModelProvider, key: string) => void;
|
||||
setQwenTokens: (tokens: { accessToken: string; refreshToken?: string; expiresAt?: number }) => void;
|
||||
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
|
||||
setProcessing: (processing: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
addToHistory: (prompt: string) => void;
|
||||
|
||||
@@ -2,5 +2,11 @@
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": ".next",
|
||||
"framework": "nextjs",
|
||||
"devCommand": "npm run dev"
|
||||
"devCommand": "npm run dev",
|
||||
"env": {
|
||||
"NEXT_PUBLIC_SITE_URL": {
|
||||
"description": "The production URL of your app (e.g., https://your-app.vercel.app)",
|
||||
"value": "https://your-app.vercel.app"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user