Initial WallMuse project
This commit is contained in:
22
packages/api-client/package.json
Executable file
22
packages/api-client/package.json
Executable file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@wallmuse/api-client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wallmuse/shared": "workspace:*",
|
||||
"zod": "^3.24.4"
|
||||
}
|
||||
}
|
||||
390
packages/api-client/src/index.ts
Executable file
390
packages/api-client/src/index.ts
Executable file
@@ -0,0 +1,390 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
apiPaths,
|
||||
AppConfigResponseSchema,
|
||||
CreateGenerationRequestSchema,
|
||||
CreateGenerationResponseSchema,
|
||||
GenerationGroupSchema,
|
||||
ModelSummarySchema,
|
||||
ProviderSummarySchema,
|
||||
WebGenerationGroupSchema,
|
||||
WebUserApiKeySchema,
|
||||
WebUserSchema,
|
||||
WebWallpaperSchema,
|
||||
type CreateGenerationRequest,
|
||||
type WebCreateGenerationInput,
|
||||
type WebGenerationGroup,
|
||||
type WebSaveApiKeyInput,
|
||||
type WebUser,
|
||||
type WebUserApiKey,
|
||||
type WebWallpaper
|
||||
} from "@wallmuse/shared";
|
||||
|
||||
export type WallMuseApiClientOptions = {
|
||||
baseUrl?: string;
|
||||
accessToken?: string;
|
||||
fetchImpl?: typeof fetch;
|
||||
headers?: HeadersInit;
|
||||
};
|
||||
|
||||
export class WallMuseApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
public readonly body: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = "WallMuseApiError";
|
||||
}
|
||||
}
|
||||
|
||||
export class WallMuseApiClient {
|
||||
private readonly baseUrl: string;
|
||||
private readonly accessToken: string | undefined;
|
||||
private readonly fetchImpl: typeof fetch;
|
||||
private readonly headers: HeadersInit | undefined;
|
||||
|
||||
constructor(options: WallMuseApiClientOptions = {}) {
|
||||
this.baseUrl = options.baseUrl?.replace(/\/$/, "") ?? "";
|
||||
this.accessToken = options.accessToken;
|
||||
this.fetchImpl = options.fetchImpl ?? fetch;
|
||||
this.headers = options.headers;
|
||||
}
|
||||
|
||||
getAppConfig() {
|
||||
return this.request(apiPaths.appConfig, AppConfigResponseSchema);
|
||||
}
|
||||
|
||||
getProviders() {
|
||||
return this.request(apiPaths.providers, z.array(ProviderSummarySchema));
|
||||
}
|
||||
|
||||
getModels() {
|
||||
return this.request(apiPaths.models, z.array(ModelSummarySchema));
|
||||
}
|
||||
|
||||
createGeneration(input: CreateGenerationRequest) {
|
||||
const body = CreateGenerationRequestSchema.parse(input);
|
||||
return this.request(apiPaths.generations, CreateGenerationResponseSchema, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
getGenerationGroup(id: string) {
|
||||
return this.request(apiPaths.generationGroup(id), GenerationGroupSchema);
|
||||
}
|
||||
|
||||
getCurrentUser(): Promise<WebUser> {
|
||||
return this.request("/api/v1/me", WebUserSchema);
|
||||
}
|
||||
|
||||
listWallpapers(): Promise<WebWallpaper[]> {
|
||||
return this.request("/api/v1/wallpapers", z.array(WebWallpaperSchema));
|
||||
}
|
||||
|
||||
listGenerationHistory(): Promise<WebGenerationGroup[]> {
|
||||
return this.request("/api/v1/generation-groups", z.array(WebGenerationGroupSchema));
|
||||
}
|
||||
|
||||
createWebGeneration(input: WebCreateGenerationInput): Promise<WebGenerationGroup> {
|
||||
return this.request("/api/v1/generations", WebGenerationGroupSchema, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
}
|
||||
|
||||
getWebGenerationGroup(id: string): Promise<WebGenerationGroup> {
|
||||
return this.request(`/api/v1/generation-groups/${id}`, WebGenerationGroupSchema);
|
||||
}
|
||||
|
||||
listApiKeys(): Promise<WebUserApiKey[]> {
|
||||
return this.request("/api/v1/user-api-keys", z.array(WebUserApiKeySchema));
|
||||
}
|
||||
|
||||
saveApiKey(input: WebSaveApiKeyInput): Promise<WebUserApiKey> {
|
||||
return this.request("/api/v1/user-api-keys", WebUserApiKeySchema, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
}
|
||||
|
||||
private async request<TSchema extends z.ZodTypeAny>(
|
||||
path: string,
|
||||
schema: TSchema,
|
||||
init: RequestInit = {}
|
||||
): Promise<z.output<TSchema>> {
|
||||
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(this.accessToken ? { authorization: `Bearer ${this.accessToken}` } : {}),
|
||||
...this.headers,
|
||||
...init.headers
|
||||
}
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
const body = text.length > 0 ? JSON.parse(text) : null;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new WallMuseApiError(`WallMuse API request failed: ${response.status}`, response.status, body);
|
||||
}
|
||||
|
||||
return schema.parse(body) as z.output<TSchema>;
|
||||
}
|
||||
}
|
||||
|
||||
type RuntimeEnv = {
|
||||
process?: {
|
||||
env?: Record<string, string | undefined>;
|
||||
};
|
||||
};
|
||||
|
||||
const getPublicApiBaseUrl = (): string =>
|
||||
(globalThis as RuntimeEnv).process?.env?.NEXT_PUBLIC_WALLMUSE_API_BASE ?? "";
|
||||
|
||||
const publicApiBaseUrl = getPublicApiBaseUrl();
|
||||
|
||||
const browserClient = new WallMuseApiClient({
|
||||
baseUrl: publicApiBaseUrl,
|
||||
fetchImpl: async (input, init) => {
|
||||
if (!publicApiBaseUrl) {
|
||||
return mockFetch(input, init);
|
||||
}
|
||||
return fetch(input, { ...init, credentials: "include" });
|
||||
}
|
||||
});
|
||||
|
||||
export const wallMuseApi = {
|
||||
getCurrentUser: () => browserClient.getCurrentUser(),
|
||||
listWallpapers: () => browserClient.listWallpapers(),
|
||||
listGenerationHistory: () => browserClient.listGenerationHistory(),
|
||||
createGeneration: (input: WebCreateGenerationInput) => browserClient.createWebGeneration(input),
|
||||
getGenerationGroup: (id: string) => browserClient.getWebGenerationGroup(id),
|
||||
listApiKeys: () => browserClient.listApiKeys(),
|
||||
saveApiKey: (input: WebSaveApiKeyInput) => browserClient.saveApiKey(input)
|
||||
};
|
||||
|
||||
const image = (id: string, width: number, height: number) =>
|
||||
`https://images.unsplash.com/${id}?auto=format&fit=crop&w=${width}&h=${height}&q=86`;
|
||||
|
||||
const mockModelId = "00000000-0000-4000-8000-000000000101";
|
||||
|
||||
const mockUser: WebUser = {
|
||||
id: "usr_demo",
|
||||
name: "Feng Lee",
|
||||
email: "feng@example.com",
|
||||
avatarInitials: "FL",
|
||||
theme: "system"
|
||||
};
|
||||
|
||||
const mockWallpapers: WebWallpaper[] = [
|
||||
{
|
||||
id: "wp-aurora",
|
||||
title: "Aurora Glass Valley",
|
||||
prompt: "A crystalline valley under soft aurora lights, cinematic wallpaper",
|
||||
imageUrl: image("photo-1500530855697-b586d89ba3ee", 1200, 900),
|
||||
ratio: "4:3",
|
||||
resolution: "4k",
|
||||
style: "Nature",
|
||||
model: "Seedream",
|
||||
likes: 1842,
|
||||
downloads: 930,
|
||||
colors: ["#9bd5ff", "#eaf7ff"],
|
||||
createdAt: "2026-05-09T08:00:00.000Z",
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: "wp-desktop-mist",
|
||||
title: "Misty Desktop Lake",
|
||||
prompt: "A calm alpine lake with glass reflections and airy blue mist",
|
||||
imageUrl: image("photo-1501785888041-af3ef285b470", 1440, 810),
|
||||
ratio: "16:9",
|
||||
resolution: "4k",
|
||||
style: "Landscape",
|
||||
model: "Qwen Image",
|
||||
likes: 1260,
|
||||
downloads: 704,
|
||||
colors: ["#d8ecff", "#607d9b"],
|
||||
createdAt: "2026-05-09T08:10:00.000Z"
|
||||
},
|
||||
{
|
||||
id: "wp-mobile-neon",
|
||||
title: "Blue Neon Alley",
|
||||
prompt: "A rainy futuristic alley with low saturation blue neon",
|
||||
imageUrl: image("photo-1519608487953-e999c86e7455", 900, 1600),
|
||||
ratio: "9:16",
|
||||
resolution: "2k",
|
||||
style: "Cyberpunk",
|
||||
model: "OpenAI Compatible",
|
||||
likes: 902,
|
||||
downloads: 388,
|
||||
colors: ["#0e2a4e", "#66aaff"],
|
||||
createdAt: "2026-05-09T08:20:00.000Z"
|
||||
},
|
||||
{
|
||||
id: "wp-minimal-wave",
|
||||
title: "Minimal Ice Wave",
|
||||
prompt: "Minimal translucent ice waves, soft studio light, clean wallpaper",
|
||||
imageUrl: image("photo-1493246507139-91e8fad9978e", 1200, 900),
|
||||
ratio: "4:3",
|
||||
resolution: "2k",
|
||||
style: "Minimal",
|
||||
model: "FLUX",
|
||||
likes: 740,
|
||||
downloads: 284,
|
||||
colors: ["#f4fbff", "#b5cbe2"],
|
||||
createdAt: "2026-05-09T08:30:00.000Z"
|
||||
},
|
||||
{
|
||||
id: "wp-space-dawn",
|
||||
title: "Orbital Dawn",
|
||||
prompt: "A quiet orbital sunrise above a blue planet, premium wallpaper",
|
||||
imageUrl: image("photo-1446776811953-b23d57bd21aa", 1440, 810),
|
||||
ratio: "16:9",
|
||||
resolution: "4k",
|
||||
style: "Space",
|
||||
model: "Seedream",
|
||||
likes: 2014,
|
||||
downloads: 1160,
|
||||
colors: ["#07111f", "#66aaff"],
|
||||
createdAt: "2026-05-09T08:40:00.000Z",
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: "wp-architecture",
|
||||
title: "Soft Concrete Atrium",
|
||||
prompt: "Modern architectural atrium, cool daylight, gentle reflections",
|
||||
imageUrl: image("photo-1497366754035-f200968a6e72", 1200, 900),
|
||||
ratio: "4:3",
|
||||
resolution: "2k",
|
||||
style: "Architecture",
|
||||
model: "Qwen Image",
|
||||
likes: 580,
|
||||
downloads: 210,
|
||||
colors: ["#eef6ff", "#8293a8"],
|
||||
createdAt: "2026-05-09T08:50:00.000Z"
|
||||
}
|
||||
];
|
||||
|
||||
const mockGenerationHistory: WebGenerationGroup[] = [
|
||||
{
|
||||
id: "gen-blue-canyon",
|
||||
prompt: "A blue glass canyon with a silent river and soft morning haze",
|
||||
status: "succeeded",
|
||||
style: "Landscape",
|
||||
model: "Seedream",
|
||||
resolution: "4k",
|
||||
consistencyScore: 94,
|
||||
createdAt: "2026-05-09T09:00:00.000Z",
|
||||
assets: [
|
||||
{
|
||||
id: "asset-desktop",
|
||||
label: "Desktop",
|
||||
ratio: "16:9",
|
||||
width: 3840,
|
||||
height: 2160,
|
||||
imageUrl: image("photo-1500534314209-a25ddb2bd429", 1440, 810)
|
||||
},
|
||||
{
|
||||
id: "asset-mobile",
|
||||
label: "Mobile",
|
||||
ratio: "9:16",
|
||||
width: 2160,
|
||||
height: 3840,
|
||||
imageUrl: image("photo-1500534314209-a25ddb2bd429", 900, 1600)
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const mockApiKeys: WebUserApiKey[] = [
|
||||
{
|
||||
id: "key-openai-compatible",
|
||||
provider: "OpenAI Compatible",
|
||||
baseUrl: "https://api.wallmuse-dev.example/v1",
|
||||
model: "gpt-image-1",
|
||||
maskedKey: "sk-...R7m",
|
||||
isDefault: true,
|
||||
status: "connected",
|
||||
updatedAt: "2026-05-09T08:45:00.000Z"
|
||||
}
|
||||
];
|
||||
|
||||
async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
const path = url.startsWith("http") ? new URL(url).pathname : url;
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
if (path === "/api/v1/me") return json(mockUser);
|
||||
if (path === "/api/v1/wallpapers") return json(mockWallpapers);
|
||||
if (path === "/api/v1/generation-groups") return json(mockGenerationHistory);
|
||||
if (path.startsWith("/api/v1/generation-groups/")) {
|
||||
const id = path.split("/").pop();
|
||||
return json(mockGenerationHistory.find((item) => item.id === id) ?? mockGenerationHistory[0]);
|
||||
}
|
||||
if (path === "/api/v1/generations" && method === "POST") {
|
||||
return json(createMockGeneration(init?.body ? JSON.parse(String(init.body)) : undefined));
|
||||
}
|
||||
if (path === "/api/v1/user-api-keys") {
|
||||
if (method === "POST") {
|
||||
const body = init?.body ? (JSON.parse(String(init.body)) as WebSaveApiKeyInput) : undefined;
|
||||
return json({
|
||||
id: "key-new-api",
|
||||
provider: body?.provider ?? "OpenAI Compatible",
|
||||
baseUrl: body?.baseUrl ?? "https://api.example.com/v1",
|
||||
model: body?.model ?? "gpt-image-1",
|
||||
maskedKey: "sk-...9Kp",
|
||||
isDefault: body?.isDefault ?? true,
|
||||
status: "connected",
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
return json(mockApiKeys);
|
||||
}
|
||||
|
||||
return json({ error: `Mock API route not implemented: ${path}` }, 404);
|
||||
}
|
||||
|
||||
function createMockGeneration(input?: WebCreateGenerationInput): WebGenerationGroup {
|
||||
return {
|
||||
id: "gen-live-preview",
|
||||
prompt: input?.prompt ?? "A clean blue wallpaper with glass light",
|
||||
negativePrompt: input?.negativePrompt,
|
||||
status: "succeeded",
|
||||
style: input?.style ?? "Nature",
|
||||
model: input?.model ?? mockModelId,
|
||||
resolution: input?.resolution ?? "2k",
|
||||
consistencyScore: 91,
|
||||
createdAt: new Date().toISOString(),
|
||||
assets: [
|
||||
{
|
||||
id: "asset-live-desktop",
|
||||
label: "Desktop",
|
||||
ratio: "16:9",
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
imageUrl: image("photo-1506744038136-46273834b3fb", 1440, 810)
|
||||
},
|
||||
{
|
||||
id: "asset-live-mobile",
|
||||
label: "Mobile",
|
||||
ratio: "9:16",
|
||||
width: 1440,
|
||||
height: 2560,
|
||||
imageUrl: image("photo-1506744038136-46273834b3fb", 900, 1600)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function json(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
export * from "@wallmuse/shared";
|
||||
10
packages/api-client/tsconfig.json
Executable file
10
packages/api-client/tsconfig.json
Executable file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user