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:
Gemini AI
2025-12-25 23:17:55 +04:00
Unverified
parent f510683e18
commit 07dbe552f7
10 changed files with 735 additions and 100 deletions

View 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";

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

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

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

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

View File

@@ -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 && (

View File

@@ -1,8 +1,10 @@
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
import OllamaCloudService from "./ollama-cloud";
import ZaiPlanService from "./zai-plan";
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
export interface ModelAdapterConfig {
qwen?: QwenOAuthConfig;
ollama?: {
apiKey?: string;
endpoint?: string;
@@ -17,12 +19,27 @@ export interface ModelAdapterConfig {
export class ModelAdapter {
private ollamaService: OllamaCloudService;
private zaiService: ZaiPlanService;
private qwenService = qwenOAuthService;
private preferredProvider: ModelProvider;
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 {
@@ -37,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[]
@@ -46,6 +90,9 @@ export class ModelAdapter {
let service: any;
switch (provider) {
case "qwen":
service = this.qwenService;
break;
case "ollama":
service = this.ollamaService;
break;
@@ -70,22 +117,26 @@ 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 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);
}
@@ -98,6 +149,9 @@ export class ModelAdapter {
let service: any;
switch (provider) {
case "qwen":
service = this.qwenService;
break;
case "ollama":
service = this.ollamaService;
break;
@@ -117,6 +171,7 @@ export class ModelAdapter {
async listModels(provider?: ModelProvider): Promise<APIResponse<Record<ModelProvider, string[]>>> {
const fallbackModels: Record<ModelProvider, string[]> = {
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"],
};
@@ -148,6 +203,8 @@ export class ModelAdapter {
getAvailableModels(provider: ModelProvider): string[] {
switch (provider) {
case "qwen":
return this.qwenService.getAvailableModels();
case "ollama":
return this.ollamaService.getAvailableModels();
case "zai":

View File

@@ -100,10 +100,18 @@ export class OllamaCloudService {
const data = await response.json();
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;
return { success: true, data: models };
} else {
console.log("[Ollama] No API key, using fallback models");

View File

@@ -1,85 +1,371 @@
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 {
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 || DEFAULT_OAUTH_BASE;
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") {
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;
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.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(
@@ -88,15 +374,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 +387,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 +482,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 };

View File

@@ -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;