refactor: improve file upload section layout and enhance API error handling

This commit is contained in:
shiva raj badu 2026-04-15 22:38:01 +05:45
parent e12c6dcdab
commit af5ce9e33b
No known key found for this signature in database
9 changed files with 154 additions and 40 deletions

View file

@ -113,10 +113,10 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
<span className='text-[#808080] underline underline-offset-4'>Click to Upload</span> or drag &amp; 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>

View file

@ -113,10 +113,10 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
<span className='text-[#808080] underline underline-offset-4'>Click to Upload</span> or drag &amp; 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>

View file

@ -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) {

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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,

View file

@ -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;
}