refactor: improve file upload section layout and enhance API error handling
This commit is contained in:
parent
e12c6dcdab
commit
af5ce9e33b
9 changed files with 154 additions and 40 deletions
|
|
@ -113,10 +113,10 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
|
|||
<span className='text-[#808080] underline underline-offset-4'>Click to Upload</span> or drag & drop.
|
||||
</p>
|
||||
</div>
|
||||
</> : <div className="flex gap-2 items-center justify-center h-full">
|
||||
<div className="flex gap-2 items-center">
|
||||
</> : <div className="flex gap-2 items-center justify-center h-full w-fit mx-auto">
|
||||
<div className="flex gap-2 items-center justify-center mx-10 w-full">
|
||||
|
||||
<div className="w-[55px] h-[55px] ml-auto mr-0 rounded-[9px] bg-[#8E8F8F] flex items-center justify-center relative">
|
||||
<div className="w-[55px] h-[55px] rounded-[9px] bg-[#8E8F8F] flex items-center justify-center relative">
|
||||
<button className="absolute w-[16px] h-[16px] flex items-center justify-center -top-1.5 -right-1.5"
|
||||
style={{
|
||||
borderRadius: '54.545px',
|
||||
|
|
@ -132,8 +132,8 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
|
|||
|
||||
<FileText className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="w-4/5">
|
||||
<h3 className="text-[#4C4C4C] text-sm font-medium w-full truncate"> {selectedFile.name}</h3>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[#4C4C4C] text-sm font-medium line-clamp-1"> {selectedFile.name}</h3>
|
||||
<p className="text-xs font-normal text-[#808080] tracking-[-0.12px]">Presentation ( {(selectedFile.size / (1024 * 1024)).toFixed(2)} MB)</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -113,10 +113,10 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
|
|||
<span className='text-[#808080] underline underline-offset-4'>Click to Upload</span> or drag & drop.
|
||||
</p>
|
||||
</div>
|
||||
</> : <div className="flex gap-2 items-center justify-center h-full">
|
||||
<div className="flex gap-2 items-center">
|
||||
</> : <div className="flex gap-2 items-center justify-center h-full w-fit mx-auto">
|
||||
<div className="flex gap-2 items-center justify-center mx-10 w-full">
|
||||
|
||||
<div className="w-[55px] h-[55px] ml-auto mr-0 rounded-[9px] bg-[#8E8F8F] flex items-center justify-center relative">
|
||||
<div className="w-[55px] h-[55px] rounded-[9px] bg-[#8E8F8F] flex items-center justify-center relative">
|
||||
<button className="absolute w-[16px] h-[16px] flex items-center justify-center -top-1.5 -right-1.5"
|
||||
style={{
|
||||
borderRadius: '54.545px',
|
||||
|
|
@ -132,8 +132,8 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
|
|||
|
||||
<FileText className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="w-4/5">
|
||||
<h3 className="text-[#4C4C4C] text-sm font-medium w-full truncate"> {selectedFile.name}</h3>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[#4C4C4C] text-sm font-medium line-clamp-1"> {selectedFile.name}</h3>
|
||||
<p className="text-xs font-normal text-[#808080] tracking-[-0.12px]">Presentation ( {(selectedFile.size / (1024 * 1024)).toFixed(2)} MB)</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,49 @@
|
|||
// API Error Response Interface
|
||||
interface ApiErrorResponse {
|
||||
detail?: string;
|
||||
detail?: unknown;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// API Response Handler Utility
|
||||
export class ApiResponseHandler {
|
||||
private static normalizeErrorDetail(detail: unknown): string | null {
|
||||
if (!detail) return null;
|
||||
|
||||
if (typeof detail === "string") {
|
||||
return detail;
|
||||
}
|
||||
|
||||
if (Array.isArray(detail)) {
|
||||
const parts = detail
|
||||
.map((item) => {
|
||||
if (typeof item === "string") return item;
|
||||
if (item && typeof item === "object") {
|
||||
const maybeMsg = (item as { msg?: unknown }).msg;
|
||||
const maybeLoc = (item as { loc?: unknown }).loc;
|
||||
const locPath = Array.isArray(maybeLoc)
|
||||
? maybeLoc
|
||||
.filter((v) => typeof v === "string" || typeof v === "number")
|
||||
.join(".")
|
||||
: "";
|
||||
if (typeof maybeMsg === "string") {
|
||||
return locPath ? `${locPath}: ${maybeMsg}` : maybeMsg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((v): v is string => Boolean(v));
|
||||
|
||||
return parts.length ? parts.join("; ") : JSON.stringify(detail);
|
||||
}
|
||||
|
||||
if (typeof detail === "object") {
|
||||
return JSON.stringify(detail);
|
||||
}
|
||||
|
||||
return String(detail);
|
||||
}
|
||||
|
||||
|
||||
static async handleResponse(response: Response, defaultErrorMessage: string): Promise<any> {
|
||||
// Handle successful responses
|
||||
|
|
@ -32,8 +69,9 @@ export class ApiResponseHandler {
|
|||
const errorData: ApiErrorResponse = await response.json();
|
||||
|
||||
// Extract error message in order of preference
|
||||
if (errorData.detail) {
|
||||
errorMessage = errorData.detail;
|
||||
const normalizedDetail = this.normalizeErrorDetail(errorData.detail);
|
||||
if (normalizedDetail) {
|
||||
errorMessage = normalizedDetail;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
} else if (errorData.error) {
|
||||
|
|
@ -63,8 +101,9 @@ export class ApiResponseHandler {
|
|||
const errorData: ApiErrorResponse = await response.json();
|
||||
|
||||
// Extract error message in order of preference
|
||||
if (errorData.detail) {
|
||||
errorMessage = errorData.detail;
|
||||
const normalizedDetail = this.normalizeErrorDetail(errorData.detail);
|
||||
if (normalizedDetail) {
|
||||
errorMessage = normalizedDetail;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
} else if (errorData.error) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { getApiUrl } from "@/utils/api";
|
||||
import {
|
||||
getHeader,
|
||||
} from "@/app/(presentation-generator)/services/api/header";
|
||||
import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/api-error-handler";
|
||||
import { getApiUrl } from "@/utils/api";
|
||||
|
||||
export interface PresentationResponse {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { getApiUrl } from "@/utils/api";
|
||||
import { getHeaderForFormData } from "./header";
|
||||
import { ApiResponseHandler } from "./api-error-handler";
|
||||
import { ImageAssetResponse } from "./types";
|
||||
import { getApiUrl } from "@/utils/api";
|
||||
|
||||
interface StockSearchOptions {
|
||||
provider?: string;
|
||||
apiKey?: string;
|
||||
strictApiKey?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export class ImagesApi {
|
||||
|
|
@ -43,6 +49,41 @@ export class ImagesApi {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async searchStockImages(
|
||||
query: string,
|
||||
limit: number = 12,
|
||||
options: StockSearchOptions = {}
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
query,
|
||||
limit: String(limit),
|
||||
});
|
||||
const normalizedProvider = (options.provider || "").trim().toLowerCase();
|
||||
if (normalizedProvider) {
|
||||
params.set("provider", normalizedProvider);
|
||||
}
|
||||
if (options.strictApiKey) {
|
||||
params.set("strict_api_key", "true");
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
const trimmedApiKey = (options.apiKey || "").trim();
|
||||
if (trimmedApiKey) {
|
||||
headers["X-Provider-Api-Key"] = trimmedApiKey;
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/images/search?${params.toString()}`), {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
return await ApiResponseHandler.handleResponse(response, "Failed to search stock images") as string[];
|
||||
} catch (error:any) {
|
||||
console.log("Stock image search error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { getApiUrl } from "@/utils/api";
|
||||
import { getHeader, getHeaderForFormData } from "./header";
|
||||
import { IconSearch, ImageGenerate, ImageSearch, PreviousGeneratedImagesResponse } from "./params";
|
||||
import { ApiResponseHandler } from "./api-error-handler";
|
||||
import { getApiUrl } from "@/utils/api";
|
||||
|
||||
export class PresentationGenerationApi {
|
||||
private static readonly DECOMPOSE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
static async uploadDoc(documents: File[]) {
|
||||
const formData = new FormData();
|
||||
|
||||
|
|
@ -31,10 +29,10 @@ export class PresentationGenerationApi {
|
|||
}
|
||||
}
|
||||
|
||||
static async decomposeDocuments(documentKeys: string[]) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.DECOMPOSE_TIMEOUT_MS);
|
||||
|
||||
static async decomposeDocuments(
|
||||
documentKeys: string[],
|
||||
language?: string | null
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
getApiUrl(`/api/v1/ppt/files/decompose`),
|
||||
|
|
@ -43,21 +41,16 @@ export class PresentationGenerationApi {
|
|||
headers: getHeader(),
|
||||
body: JSON.stringify({
|
||||
file_paths: documentKeys,
|
||||
language: language ?? null,
|
||||
}),
|
||||
cache: "no-cache",
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
|
||||
return await ApiResponseHandler.handleResponse(response, "Failed to decompose documents");
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
throw new Error("File decomposition timed out after 10 minutes");
|
||||
}
|
||||
console.error("Error in Decompose Files", error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
import { getApiUrl } from "@/utils/api";
|
||||
import { ApiResponseHandler } from "./api-error-handler";
|
||||
import { getHeader } from "./header";
|
||||
|
||||
export interface CloneTemplatePayload {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CloneLayoutPayload {
|
||||
template_id: string;
|
||||
layout_id: string;
|
||||
layout_name?: string;
|
||||
}
|
||||
|
||||
class TemplateService {
|
||||
|
||||
static async getCustomTemplateSummaries() {
|
||||
try {
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template-management/summary`));
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template/all`),);
|
||||
return await ApiResponseHandler.handleResponse(response, "Failed to get custom template summaries");
|
||||
} catch (error) {
|
||||
console.error("Failed to get custom template summaries", error);
|
||||
|
|
@ -15,7 +28,7 @@ class TemplateService {
|
|||
|
||||
static async getCustomTemplateDetails(templateId: string) {
|
||||
try {
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template-management/get-templates/${templateId}`));
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template/${templateId}/layouts`),);
|
||||
return await ApiResponseHandler.handleResponse(response, "Failed to get custom template details");
|
||||
} catch (error) {
|
||||
console.error("Failed to get custom template details", error);
|
||||
|
|
@ -25,13 +38,41 @@ class TemplateService {
|
|||
|
||||
static async deleteCustomTemplate(presentationId: string) {
|
||||
try {
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template-management/delete-templates/${presentationId}`), { method: "DELETE" });
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template-management/delete-templates/${presentationId}`), { method: "DELETE", headers: getHeader() });
|
||||
return await ApiResponseHandler.handleResponseWithResult(response, "Failed to delete custom template");
|
||||
} catch (error) {
|
||||
console.error("Failed to delete custom template", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async cloneCustomTemplate(payload: CloneTemplatePayload) {
|
||||
try {
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template/clone`), {
|
||||
method: "POST",
|
||||
headers: getHeader(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return await ApiResponseHandler.handleResponse(response, "Failed to clone template");
|
||||
} catch (error) {
|
||||
console.error("Failed to clone template", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async cloneTemplateLayout(payload: CloneLayoutPayload) {
|
||||
try {
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/template/slide-layout/clone`), {
|
||||
method: "POST",
|
||||
headers: getHeader(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return await ApiResponseHandler.handleResponse(response, "Failed to clone layout");
|
||||
} catch (error) {
|
||||
console.error("Failed to clone layout", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TemplateService;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { getApiUrl } from "@/utils/api"
|
||||
import { ApiResponseHandler } from "./api-error-handler"
|
||||
import { getHeader, getHeaderForFormData } from "./header"
|
||||
import { Theme, ThemeParams } from "./types"
|
||||
import { getApiUrl } from "@/utils/api"
|
||||
|
||||
|
||||
|
||||
|
|
@ -90,8 +90,8 @@ class ThemeApi {
|
|||
static async uploadFont(font: File) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", font);
|
||||
const response = await fetch(getApiUrl(`/api/v1/ppt/fonts/upload`), {
|
||||
formData.append("font_file", font);
|
||||
const response: any = await fetch(getApiUrl(`/api/v1/ppt/fonts/upload`), {
|
||||
method: "POST",
|
||||
headers: getHeaderForFormData(),
|
||||
body: formData,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ export interface DeplotResponse {
|
|||
}
|
||||
|
||||
export interface ImageAssetResponse {
|
||||
message: string;
|
||||
path: string;
|
||||
id: string;
|
||||
file_url?: string;
|
||||
message: string;
|
||||
path: string;
|
||||
id: string;
|
||||
file_url?: string;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue