Deploy to production
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3005/api';
|
||||
|
||||
const getHeaders = () => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||
};
|
||||
};
|
||||
|
||||
export const api = {
|
||||
// Auth
|
||||
register: async (email, password) => {
|
||||
const res = await fetch(`${API_URL}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data; // returns { status: 'pending' | 'active', token, profile }
|
||||
},
|
||||
|
||||
login: async (email, password) => {
|
||||
const res = await fetch(`${API_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
getMe: async () => {
|
||||
const res = await fetch(`${API_URL}/auth/me`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Projects
|
||||
getProjects: async () => {
|
||||
const res = await fetch(`${API_URL}/projects`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
getProject: async (id) => {
|
||||
const res = await fetch(`${API_URL}/projects/${id}`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
saveProject: async (projectData) => {
|
||||
const res = await fetch(`${API_URL}/projects`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(projectData)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Admin
|
||||
getUsers: async () => {
|
||||
const res = await fetch(`${API_URL}/users`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateUser: async (id, updateData) => {
|
||||
const res = await fetch(`${API_URL}/users/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Credits
|
||||
deductCredit: async () => {
|
||||
const res = await fetch(`${API_URL}/users/deduct-credit`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Storage
|
||||
uploadAsset: async (fileBlob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileBlob);
|
||||
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||
|
||||
const res = await fetch(`${API_URL}/upload`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
return data.url;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Utility functions for API requests with exponential backoff and polling.
|
||||
*/
|
||||
|
||||
export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export interface FetchOptions extends RequestInit {
|
||||
maxRetries?: number;
|
||||
baseDelayMs?: number;
|
||||
}
|
||||
|
||||
export async function fetchWithRetry(url: string, options: FetchOptions = {}): Promise<Response> {
|
||||
const { maxRetries = 3, baseDelayMs = 1000, ...fetchOptions } = options;
|
||||
let retries = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const response = await fetch(url, fetchOptions);
|
||||
if (response.ok) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Don't retry on 4xx errors (client errors)
|
||||
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
||||
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// If it's a 429 (Rate Limit) or 5xx, we throw to trigger a retry
|
||||
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
||||
} catch (error) {
|
||||
if (retries >= maxRetries) {
|
||||
throw error;
|
||||
}
|
||||
retries++;
|
||||
// Exponential backoff with some jitter
|
||||
const waitTime = (baseDelayMs * Math.pow(2, retries - 1)) + (Math.random() * 500);
|
||||
console.warn(`Request failed, retrying in ${Math.round(waitTime)}ms... (Attempt ${retries}/${maxRetries})`);
|
||||
await delay(waitTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PollOptions {
|
||||
intervalMs?: number;
|
||||
maxAttempts?: number;
|
||||
onProgress?: (progress: number) => void;
|
||||
}
|
||||
|
||||
export async function pollStatus<T>(
|
||||
pollFn: () => Promise<{ status: 'processing' | 'success' | 'failed', data?: T, progress?: number }>,
|
||||
options: PollOptions = {}
|
||||
): Promise<T> {
|
||||
const { intervalMs = 2000, maxAttempts = 150, onProgress } = options; // Default to 5 minutes total polling
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const result = await pollFn();
|
||||
|
||||
if (result.status === 'success' && result.data) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
if (result.status === 'failed') {
|
||||
throw new Error("Task failed during processing on the remote server.");
|
||||
}
|
||||
|
||||
if (result.progress !== undefined && onProgress) {
|
||||
onProgress(result.progress);
|
||||
}
|
||||
} catch (err) {
|
||||
// If the poll request itself fails, we log it but keep trying until maxAttempts
|
||||
console.warn("Polling request failed, will retry next interval:", err);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
await delay(intervalMs);
|
||||
}
|
||||
|
||||
throw new Error("Polling timeout exceeded.");
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { fetchWithRetry, pollStatus } from './apiUtils';
|
||||
|
||||
const MESHY_BASE_URL = 'https://api.meshy.ai/openapi/v1';
|
||||
|
||||
export interface MeshyTaskResponse {
|
||||
result: string;
|
||||
progress: number;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED';
|
||||
task_error?: { message: string };
|
||||
model_urls?: { glb: string; usdz?: string };
|
||||
}
|
||||
|
||||
export async function createMeshyTask(apiKey: string, imageBase64DataUrl: string): Promise<string> {
|
||||
const response = await fetchWithRetry(`${MESHY_BASE_URL}/image-to-3d`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image_url: imageBase64DataUrl,
|
||||
enable_pbr: true,
|
||||
should_remesh: true
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.result; // Task ID
|
||||
}
|
||||
|
||||
export async function waitForMeshyTask(apiKey: string, taskId: string, onProgress?: (p: number) => void): Promise<string> {
|
||||
return pollStatus(async () => {
|
||||
const response = await fetchWithRetry(`${MESHY_BASE_URL}/image-to-3d/${taskId}`, {
|
||||
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||
maxRetries: 2,
|
||||
baseDelayMs: 500
|
||||
});
|
||||
|
||||
const data: MeshyTaskResponse = await response.json();
|
||||
|
||||
if (data.status === 'SUCCEEDED' && data.model_urls?.glb) {
|
||||
return { status: 'success', data: data.model_urls.glb };
|
||||
}
|
||||
|
||||
if (data.status === 'FAILED' || data.status === 'EXPIRED') {
|
||||
console.error("Meshy task failed:", data.task_error);
|
||||
return { status: 'failed' };
|
||||
}
|
||||
|
||||
return { status: 'processing', progress: data.progress || 0 };
|
||||
}, {
|
||||
intervalMs: 3000,
|
||||
maxAttempts: 100, // 5 minutes max wait
|
||||
onProgress
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { fetchWithRetry, pollStatus } from './apiUtils';
|
||||
|
||||
const TRIPO_BASE_URL = 'https://api.tripo3d.ai/v2/openapi';
|
||||
|
||||
export interface TripoTaskResponse {
|
||||
code: number;
|
||||
data: {
|
||||
task_id: string;
|
||||
status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled';
|
||||
progress: number;
|
||||
output?: {
|
||||
model?: string; // URL to the model
|
||||
base_model?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Upload Image
|
||||
export async function uploadImageToTripo(apiKey: string, imageFile: File): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', imageFile);
|
||||
|
||||
const response = await fetchWithRetry(`${TRIPO_BASE_URL}/upload`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.code !== 0 || !data.data.image_token) {
|
||||
throw new Error("Failed to upload image to Tripo");
|
||||
}
|
||||
return data.data.image_token;
|
||||
}
|
||||
|
||||
// 2. Create Task
|
||||
export async function createTripoTask(apiKey: string, imageToken: string): Promise<string> {
|
||||
const response = await fetchWithRetry(`${TRIPO_BASE_URL}/task`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'image_to_model',
|
||||
file: { type: 'jpg', file_token: imageToken }
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.code !== 0 || !data.data.task_id) {
|
||||
throw new Error("Failed to create Tripo task");
|
||||
}
|
||||
return data.data.task_id;
|
||||
}
|
||||
|
||||
// 3. Poll Task
|
||||
export async function waitForTripoTask(apiKey: string, taskId: string, onProgress?: (p: number) => void): Promise<string> {
|
||||
return pollStatus(async () => {
|
||||
const response = await fetchWithRetry(`${TRIPO_BASE_URL}/task/${taskId}`, {
|
||||
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||
maxRetries: 2,
|
||||
baseDelayMs: 500
|
||||
});
|
||||
|
||||
const json: TripoTaskResponse = await response.json();
|
||||
const data = json.data;
|
||||
|
||||
if (data.status === 'success' && data.output?.model) {
|
||||
return { status: 'success', data: data.output.model };
|
||||
}
|
||||
|
||||
if (data.status === 'failed' || data.status === 'cancelled') {
|
||||
return { status: 'failed' };
|
||||
}
|
||||
|
||||
return { status: 'processing', progress: data.progress || 0 };
|
||||
}, {
|
||||
intervalMs: 3000,
|
||||
maxAttempts: 100,
|
||||
onProgress
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { api } from './api';
|
||||
|
||||
export const uploadAssetToStorage = async (file: Blob | File, bucket: string, path: string): Promise<string> => {
|
||||
try {
|
||||
const url = await api.uploadAsset(file);
|
||||
return url;
|
||||
} catch (err) {
|
||||
console.warn("Storage upload exception, falling back to data URL.", err);
|
||||
return await fileToDataUrl(file);
|
||||
}
|
||||
};
|
||||
|
||||
export const fileToDataUrl = (file: Blob | File): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user