first commit

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
HouYunFei
2026-05-19 07:53:53 +08:00
commit 472bf8b732
133 changed files with 16869 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
export type AiConfig = {
baseUrl: string;
apiKey: string;
model: string;
imageModel: string;
textModel: string;
models: string[];
quality: string;
size: string;
count: string;
};
export const CONFIG_STORE_KEY = "infinite-canvas:ai_config_store";
export const defaultConfig: AiConfig = {
baseUrl: "https://api.openai.com",
apiKey: "",
model: "gpt-image-2",
imageModel: "gpt-image-2",
textModel: "gpt-5.5",
models: [],
quality: "auto",
size: "1:1",
count: "1",
};
export function normalizeBaseUrl(value: string) {
return value.trim().replace(/\/+$/, "");
}
export function buildApiUrl(baseUrl: string, path: string) {
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
const apiBaseUrl = normalizedBaseUrl.endsWith("/v1") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`;
return `${apiBaseUrl}${path}`;
}
+63
View File
@@ -0,0 +1,63 @@
export type CanvasColorTheme = "light" | "dark";
export type CanvasBackgroundMode = "dots" | "lines" | "blank";
export const canvasThemes = {
light: {
canvas: {
background: "#f4f2ed",
dot: "rgba(68,64,60,.28)",
line: "rgba(68,64,60,.12)",
selectionStroke: "#1c1917",
selectionFill: "rgba(28,25,23,.06)",
},
node: {
label: "#57534e",
fill: "#e7e5df",
panel: "#fbfaf7",
stroke: "#d6d3ca",
activeStroke: "#1c1917",
placeholder: "#8a8479",
text: "#292524",
muted: "#78716c",
faint: "#a8a29e",
},
toolbar: {
panel: "rgba(251,250,247,.96)",
border: "#d6d3ca",
item: "#57534e",
itemHover: "#e7e5df",
activeBg: "#e7e5df",
activeText: "#292524",
},
},
dark: {
canvas: {
background: "#181715",
dot: "rgba(245,245,244,.24)",
line: "rgba(245,245,244,.10)",
selectionStroke: "#fafaf9",
selectionFill: "rgba(250,250,249,.10)",
},
node: {
label: "#d6d3d1",
fill: "#292524",
panel: "#1f1d1a",
stroke: "#44403c",
activeStroke: "#fafaf9",
placeholder: "#a8a29e",
text: "#f5f5f4",
muted: "#d6d3d1",
faint: "#78716c",
},
toolbar: {
panel: "rgba(31,29,26,.96)",
border: "#44403c",
item: "#d6d3d1",
itemHover: "#292524",
activeBg: "#3a3631",
activeText: "#f5f5f4",
},
},
} as const;
export type CanvasTheme = (typeof canvasThemes)[CanvasColorTheme];
+3
View File
@@ -0,0 +1,3 @@
export function createId() {
return crypto.randomUUID();
}
+62
View File
@@ -0,0 +1,62 @@
import type { ReferenceImage } from "@/types/image";
export function formatBytes(bytes: number) {
if (!Number.isFinite(bytes) || bytes <= 0) {
return "";
}
const units = ["B", "KB", "MB", "GB"];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex += 1;
}
return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
}
export function formatDuration(ms: number) {
const value = Math.max(0, Math.floor(ms / 1000));
const minutes = Math.floor(value / 60);
const seconds = value % 60;
return minutes ? `${minutes}${String(seconds).padStart(2, "0")}` : `${seconds}`;
}
export function getDataUrlByteSize(dataUrl: string) {
const base64 = dataUrl.split(",", 2)[1];
if (!base64) {
return 0;
}
const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
return Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
}
export function readFileAsDataUrl(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(new Error("读取图片失败"));
reader.readAsDataURL(file);
});
}
export function readImageMeta(dataUrl: string) {
return new Promise<{ width: number; height: number; mimeType: string }>((resolve) => {
const image = new Image();
const done = () => resolve({ width: image.naturalWidth || 1024, height: image.naturalHeight || 1024, mimeType: dataUrl.match(/^data:([^;]+)/)?.[1] || "image/png" });
image.onload = done;
image.onerror = done;
setTimeout(done, 3000);
image.src = dataUrl;
});
}
export function dataUrlToFile(image: ReferenceImage) {
const [header, content] = image.dataUrl.split(",", 2);
const mimeType = header.match(/data:(.*?);base64/)?.[1] || image.type || "image/png";
const binary = atob(content || "");
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return new File([bytes], image.name || "reference.png", { type: mimeType });
}
+34
View File
@@ -0,0 +1,34 @@
import localforage from "localforage";
import type { StateStorage } from "zustand/middleware";
localforage.config({
name: "infinite-canvas",
storeName: "app_state",
});
export const localForageStorage: StateStorage = {
getItem: async (name) => {
if (typeof window === "undefined") return null;
try {
return (await localforage.getItem<string>(name)) || null;
} catch {
return window.localStorage.getItem(name);
}
},
setItem: async (name, value) => {
if (typeof window === "undefined") return;
try {
await localforage.setItem(name, value);
} catch {
window.localStorage.setItem(name, value);
}
},
removeItem: async (name) => {
if (typeof window === "undefined") return;
try {
await localforage.removeItem(name);
} catch {
window.localStorage.removeItem(name);
}
},
};
+26
View File
@@ -0,0 +1,26 @@
import { FileText, ImagePlus, Images, Maximize2 } from "lucide-react";
export const navigationTools = [
{
slug: "canvas",
label: "我的画布",
icon: Maximize2,
},
{
slug: "image",
label: "生图工作台",
icon: ImagePlus,
},
{
slug: "prompts",
label: "提示词库",
icon: FileText,
},
{
slug: "assets",
label: "我的素材",
icon: Images,
},
] as const;
export type NavigationToolSlug = (typeof navigationTools)[number]["slug"];
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}