Initial WallMuse project

This commit is contained in:
fenglee
2026-05-09 09:12:41 +00:00
commit 3ea7d29827
91 changed files with 13136 additions and 0 deletions

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

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": [
"src/**/*.ts"
]
}