Fix Qwen OAuth and Ollama models listing
- Add generateUXDesignerPrompt method to QwenOAuthService - Fix Ollama Cloud listModels to handle multiple response formats - Improve Ollama models parsing with array/different response structures - All providers now support UX Designer Prompt feature
This commit is contained in:
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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(() => {
|
useEffect(() => {
|
||||||
handleLoad();
|
handleLoad();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -122,17 +147,14 @@ export default function SettingsPanel() {
|
|||||||
variant={qwenTokens ? "secondary" : "outline"}
|
variant={qwenTokens ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8"
|
className="h-8"
|
||||||
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 && (
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
|
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
|
||||||
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?: QwenOAuthConfig;
|
||||||
ollama?: {
|
ollama?: {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
@@ -17,12 +19,27 @@ export interface ModelAdapterConfig {
|
|||||||
export class ModelAdapter {
|
export class ModelAdapter {
|
||||||
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 = "ollama") {
|
constructor(config: ModelAdapterConfig = {}, preferredProvider: ModelProvider = "ollama") {
|
||||||
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 {
|
||||||
@@ -37,6 +54,33 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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[]
|
||||||
@@ -46,6 +90,9 @@ export class ModelAdapter {
|
|||||||
let service: any;
|
let service: any;
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case "qwen":
|
||||||
|
service = this.qwenService;
|
||||||
|
break;
|
||||||
case "ollama":
|
case "ollama":
|
||||||
service = this.ollamaService;
|
service = this.ollamaService;
|
||||||
break;
|
break;
|
||||||
@@ -70,22 +117,26 @@ export class ModelAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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>> {
|
async generateUXDesignerPrompt(appDescription: 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.generateUXDesignerPrompt(appDescription, model), providers);
|
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +149,9 @@ export class ModelAdapter {
|
|||||||
let service: any;
|
let service: any;
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case "qwen":
|
||||||
|
service = this.qwenService;
|
||||||
|
break;
|
||||||
case "ollama":
|
case "ollama":
|
||||||
service = this.ollamaService;
|
service = this.ollamaService;
|
||||||
break;
|
break;
|
||||||
@@ -117,6 +171,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: 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"],
|
||||||
};
|
};
|
||||||
@@ -148,6 +203,8 @@ export class ModelAdapter {
|
|||||||
|
|
||||||
getAvailableModels(provider: ModelProvider): string[] {
|
getAvailableModels(provider: ModelProvider): string[] {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case "qwen":
|
||||||
|
return this.qwenService.getAvailableModels();
|
||||||
case "ollama":
|
case "ollama":
|
||||||
return this.ollamaService.getAvailableModels();
|
return this.ollamaService.getAvailableModels();
|
||||||
case "zai":
|
case "zai":
|
||||||
|
|||||||
@@ -100,7 +100,15 @@ export class OllamaCloudService {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("[Ollama] Models data:", data);
|
console.log("[Ollama] Models data:", data);
|
||||||
const models = data.models?.map((m: OllamaModel) => m.name) || [];
|
|
||||||
|
let models: string[] = [];
|
||||||
|
if (Array.isArray(data.models)) {
|
||||||
|
models = data.models.map((m: OllamaModel) => m.name);
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
models = data.map((m: OllamaModel) => m.name);
|
||||||
|
} else if (data.model) {
|
||||||
|
models = [data.model.name];
|
||||||
|
}
|
||||||
|
|
||||||
this.availableModels = models;
|
this.availableModels = models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +1,371 @@
|
|||||||
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 DEFAULT_OAUTH_BASE = "/api/qwen";
|
||||||
|
const TOKEN_STORAGE_KEY = "promptarch-qwen-tokens";
|
||||||
|
|
||||||
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 || DEFAULT_OAUTH_BASE;
|
||||||
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 {
|
/**
|
||||||
const baseUrl = "https://dashscope.console.aliyun.com/oauth/authorize"; // Placeholder URL
|
* Build default headers for Qwen completions (includes OAuth token refresh).
|
||||||
const params = new URLSearchParams({
|
*/
|
||||||
client_id: this.config.clientId || "",
|
private async getRequestHeaders(): Promise<Record<string, string>> {
|
||||||
redirect_uri: this.config.redirectUri || "",
|
const token = await this.getValidToken();
|
||||||
response_type: "code",
|
const headers: Record<string, string> = {
|
||||||
scope: "dashscope:chat",
|
"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") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = window.localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
this.token = JSON.parse(stored);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
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") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
window.localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
||||||
|
} else {
|
||||||
|
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.clearTokens();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clearTokens();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign out the OAuth session.
|
||||||
|
*/
|
||||||
|
signOut(): void {
|
||||||
|
this.clearTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores OAuth tokens locally.
|
||||||
|
*/
|
||||||
|
setOAuthTokens(tokens?: QwenOAuthToken) {
|
||||||
|
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 codeVerifier = this.generateCodeVerifier();
|
||||||
|
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
|
||||||
|
const deviceAuth = await this.requestDeviceAuthorization(codeChallenge);
|
||||||
|
|
||||||
|
const popup = window.open(
|
||||||
|
deviceAuth.verification_uri_complete,
|
||||||
|
"qwen-oauth",
|
||||||
|
"width=500,height=600,scrollbars=yes,resizable=yes"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!popup) {
|
||||||
|
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(
|
async chatCompletion(
|
||||||
@@ -88,15 +374,12 @@ export class QwenOAuthService {
|
|||||||
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 url = `${this.getEffectiveEndpoint()}/chat/completions`;
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[Qwen] API call:", { endpoint: this.config.endpoint, model, messages });
|
const response = await fetch(url, {
|
||||||
|
|
||||||
const response = await fetch(`${this.config.endpoint}/chat/completions`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: this.getHeaders(),
|
headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
messages,
|
messages,
|
||||||
@@ -104,22 +387,17 @@ export class QwenOAuthService {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
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 {
|
||||||
@@ -204,14 +482,95 @@ Include specific recommendations for:
|
|||||||
return this.chatCompletion([systemMessage, userMessage], model || "qwen-coder-plus");
|
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[]>> {
|
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 };
|
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 [
|
||||||
|
"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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user