diff --git a/frontend/app/(presentation-generator)/components/ReviewWorkflow.tsx b/frontend/app/(presentation-generator)/components/ReviewWorkflow.tsx index 3f40af4..aec108d 100644 --- a/frontend/app/(presentation-generator)/components/ReviewWorkflow.tsx +++ b/frontend/app/(presentation-generator)/components/ReviewWorkflow.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useEffect, useState } from "react"; +import { apiFetch } from '../../../lib/apiFetch'; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { @@ -57,7 +58,7 @@ export default function ReviewWorkflow({ presentationId }: ReviewWorkflowProps) const fetchReviewInfo = async () => { try { - const response = await fetch(`/api/v1/ppt/presentation/${presentationId}/review`, { + const response = await apiFetch(`/api/v1/ppt/presentation/${presentationId}/review`, { headers: getHeader(), }); const data = await ApiResponseHandler.handleResponse(response, "Failed to fetch review info"); @@ -72,7 +73,7 @@ export default function ReviewWorkflow({ presentationId }: ReviewWorkflowProps) const handleStatusChange = async (newStatus: string) => { setIsUpdating(true); try { - const response = await fetch(`/api/v1/ppt/presentation/${presentationId}/status`, { + const response = await apiFetch(`/api/v1/ppt/presentation/${presentationId}/status`, { method: "PUT", headers: getHeader(), body: JSON.stringify({ status: newStatus, comment: comment || null }), @@ -93,7 +94,7 @@ export default function ReviewWorkflow({ presentationId }: ReviewWorkflowProps) if (!comment.trim()) return; setIsUpdating(true); try { - const response = await fetch(`/api/v1/ppt/presentation/${presentationId}/comment`, { + const response = await apiFetch(`/api/v1/ppt/presentation/${presentationId}/comment`, { method: "POST", headers: getHeader(), body: JSON.stringify({ comment }), diff --git a/frontend/app/(presentation-generator)/hooks/useBrandTheme.ts b/frontend/app/(presentation-generator)/hooks/useBrandTheme.ts index c534902..40b84dd 100644 --- a/frontend/app/(presentation-generator)/hooks/useBrandTheme.ts +++ b/frontend/app/(presentation-generator)/hooks/useBrandTheme.ts @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { apiFetch } from '../../../lib/apiFetch'; import { useSelector } from "react-redux"; import { RootState } from "@/store/store"; import { getHeader } from "../services/api/header"; @@ -74,7 +75,7 @@ export function useBrandTheme() { } // Fetch brand config for selected client - fetch(`/api/v1/admin/clients/${selectedClientId}/brand`, { + apiFetch(`/api/v1/admin/clients/${selectedClientId}/brand`, { headers: getHeader(), }) .then((res) => { diff --git a/frontend/app/(presentation-generator)/services/api/images.ts b/frontend/app/(presentation-generator)/services/api/images.ts index 5e8c15f..fe67307 100644 --- a/frontend/app/(presentation-generator)/services/api/images.ts +++ b/frontend/app/(presentation-generator)/services/api/images.ts @@ -1,4 +1,5 @@ import { getHeaderForFormData } from "./header"; +import { apiFetch } from '../../../../lib/apiFetch'; import { ApiResponseHandler } from "./api-error-handler"; import { ImageAssetResponse } from "./types"; @@ -9,7 +10,7 @@ export class ImagesApi { try { const formData = new FormData(); formData.append("file", file); - const response = await fetch(`/api/v1/ppt/images/upload`, { + const response = await apiFetch(`/api/v1/ppt/images/upload`, { method: "POST", headers: getHeaderForFormData(), body: formData, @@ -23,7 +24,7 @@ export class ImagesApi { static async getUploadedImages(): Promise { try { - const response = await fetch(`/api/v1/ppt/images/uploaded`); + const response = await apiFetch(`/api/v1/ppt/images/uploaded`); return await ApiResponseHandler.handleResponse(response, "Failed to get uploaded images") as ImageAssetResponse[]; } catch (error:any) { console.log("Get uploaded images error:", error); @@ -33,7 +34,7 @@ export class ImagesApi { static async deleteImage(image_id: string): Promise<{success: boolean, message?: string}> { try { - const response = await fetch(`/api/v1/ppt/images/${image_id}`, { + const response = await apiFetch(`/api/v1/ppt/images/${image_id}`, { method: "DELETE" }); return await ApiResponseHandler.handleResponse(response, "Failed to delete image") as {success: boolean, message?: string}; diff --git a/frontend/app/(presentation-generator)/services/api/template.ts b/frontend/app/(presentation-generator)/services/api/template.ts index 8f511fc..89409e0 100644 --- a/frontend/app/(presentation-generator)/services/api/template.ts +++ b/frontend/app/(presentation-generator)/services/api/template.ts @@ -1,10 +1,11 @@ import { ApiResponseHandler } from "./api-error-handler"; +import { apiFetch } from '../../../../lib/apiFetch'; class TemplateService { static async getCustomTemplateSummaries() { try { - const response = await fetch(`/api/v1/ppt/template-management/summary`,); + const response = await apiFetch(`/api/v1/ppt/template-management/summary`,); return await ApiResponseHandler.handleResponse(response, "Failed to get custom template summaries"); } catch (error) { console.error("Failed to get custom template summaries", error); @@ -14,7 +15,7 @@ class TemplateService { static async getCustomTemplateDetails(templateId: string) { try { - const response = await fetch(`/api/v1/ppt/template-management/get-templates/${templateId}`,); + const response = await apiFetch(`/api/v1/ppt/template-management/get-templates/${templateId}`,); return await ApiResponseHandler.handleResponse(response, "Failed to get custom template details"); } catch (error) { console.error("Failed to get custom template details", error); @@ -24,7 +25,7 @@ class TemplateService { static async deleteCustomTemplate(presentationId: string) { try { - const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, { method: "DELETE" }); + const response = await apiFetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, { method: "DELETE" }); return await ApiResponseHandler.handleResponseWithResult(response, "Failed to delete custom template"); } catch (error) { console.error("Failed to delete custom template", error); diff --git a/frontend/app/(presentation-generator)/services/api/wizard.ts b/frontend/app/(presentation-generator)/services/api/wizard.ts index 6c1e585..ecd1e37 100644 --- a/frontend/app/(presentation-generator)/services/api/wizard.ts +++ b/frontend/app/(presentation-generator)/services/api/wizard.ts @@ -1,4 +1,5 @@ import { getHeader, getHeaderForFormData } from "./header"; +import { apiFetch } from '../../../../lib/apiFetch'; import { ApiResponseHandler } from "./api-error-handler"; export interface ClientOption { @@ -30,7 +31,7 @@ export class WizardApi { /** Fetch clients available to the current user */ static async getClients(): Promise { try { - const response = await fetch("/api/v1/admin/clients", { + const response = await apiFetch("/api/v1/admin/clients", { method: "GET", headers: getHeader(), }); @@ -46,7 +47,7 @@ export class WizardApi { static async getMasterDecks(clientId?: string): Promise { try { const params = clientId ? `?client_id=${clientId}` : ""; - const response = await fetch(`/api/v1/admin/master-decks${params}`, { + const response = await apiFetch(`/api/v1/admin/master-decks${params}`, { method: "GET", headers: getHeader(), }); @@ -63,7 +64,7 @@ export class WizardApi { const formData = new FormData(); files.forEach((file) => formData.append("files", file)); - const response = await fetch("/api/v1/ppt/files/upload", { + const response = await apiFetch("/api/v1/ppt/files/upload", { method: "POST", headers: getHeaderForFormData(), body: formData, @@ -74,7 +75,7 @@ export class WizardApi { /** Decompose uploaded documents */ static async decomposeFiles(filePaths: string[]): Promise { - const response = await fetch("/api/v1/ppt/files/decompose", { + const response = await apiFetch("/api/v1/ppt/files/decompose", { method: "POST", headers: getHeader(), body: JSON.stringify({ file_paths: filePaths }), @@ -94,7 +95,7 @@ export class WizardApi { client_id?: string; master_deck_id?: string; }) { - const response = await fetch("/api/v1/ppt/presentation/generate/async", { + const response = await apiFetch("/api/v1/ppt/presentation/generate/async", { method: "POST", headers: getHeader(), body: JSON.stringify(params), @@ -105,7 +106,7 @@ export class WizardApi { /** Poll job status */ static async getJobStatus(jobId: string): Promise { - const response = await fetch(`/api/v1/ppt/jobs/${jobId}`, { + const response = await apiFetch(`/api/v1/ppt/jobs/${jobId}`, { method: "GET", headers: getHeader(), }); @@ -114,7 +115,7 @@ export class WizardApi { /** Cancel a job */ static async cancelJob(jobId: string): Promise { - const response = await fetch(`/api/v1/ppt/jobs/${jobId}`, { + const response = await apiFetch(`/api/v1/ppt/jobs/${jobId}`, { method: "DELETE", headers: getHeader(), }); @@ -123,7 +124,7 @@ export class WizardApi { /** Fetch URL content and extract text */ static async fetchUrl(url: string): Promise { - const response = await fetch("/api/v1/ppt/files/fetch-url", { + const response = await apiFetch("/api/v1/ppt/files/fetch-url", { method: "POST", headers: getHeader(), body: JSON.stringify({ url }), @@ -138,7 +139,7 @@ export class WizardApi { static async checkFollowUpQuestions(content: string): Promise { if (!content || content.trim().length < 10) return []; try { - const response = await fetch("/api/v1/ppt/content/follow-up-questions", { + const response = await apiFetch("/api/v1/ppt/content/follow-up-questions", { method: "POST", headers: getHeader(), body: JSON.stringify({ content }), @@ -162,7 +163,7 @@ export class WizardApi { client_id?: string; master_deck_id?: string; }) { - const response = await fetch("/api/v1/ppt/presentation/create", { + const response = await apiFetch("/api/v1/ppt/presentation/create", { method: "POST", headers: getHeader(), body: JSON.stringify(params), diff --git a/frontend/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts b/frontend/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts index 9f8fba6..ec05666 100644 --- a/frontend/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts +++ b/frontend/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useRef, useCallback, useState } from 'react'; +import { apiFetch } from '../../../../../lib/apiFetch'; import { getHeader } from '@/app/(presentation-generator)/services/api/header'; import { ApiResponseHandler } from '@/app/(presentation-generator)/services/api/api-error-handler'; import { ProcessedSlide } from '@/app/(presentation-generator)/custom-template/types'; @@ -77,7 +78,7 @@ export const useTemplateLayoutsAutoSave = ({ setSaveStatus('saving'); console.log('🔄 Auto-saving template layouts...'); - const response = await fetch('/api/v1/ppt/template/update', { + const response = await apiFetch('/api/v1/ppt/template/update', { method: 'PUT', headers: getHeader(), body: JSON.stringify({ diff --git a/frontend/app/ConfigurationInitializer.tsx b/frontend/app/ConfigurationInitializer.tsx index c7366b7..f5d9f92 100644 --- a/frontend/app/ConfigurationInitializer.tsx +++ b/frontend/app/ConfigurationInitializer.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { apiFetch } from '../lib/apiFetch'; import { setCanChangeKeys, setLLMConfig } from '@/store/slices/userConfig'; import { hasValidLLMConfig } from '@/utils/storeHelpers'; import { usePathname, useRouter } from 'next/navigation'; @@ -86,7 +87,7 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo const checkIfSelectedCustomModelIsAvailable = async (llmConfig: LLMConfig) => { try { - const response = await fetch('/api/v1/ppt/openai/models/available', { + const response = await apiFetch('/api/v1/ppt/openai/models/available', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/app/admin/analytics/page.tsx b/frontend/app/admin/analytics/page.tsx index 3a293f6..0c4fd6c 100644 --- a/frontend/app/admin/analytics/page.tsx +++ b/frontend/app/admin/analytics/page.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useEffect, useState } from 'react'; +import { apiFetch } from '../../../lib/apiFetch'; import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; import { @@ -56,7 +57,7 @@ interface AIUsageData { async function fetchAnalytics(endpoint: string, clientId?: string) { const params = clientId ? `?client_id=${clientId}` : ''; - const response = await fetch(`/api/v1/admin/analytics/${endpoint}${params}`, { + const response = await apiFetch(`/api/v1/admin/analytics/${endpoint}${params}`, { headers: getHeader(), }); if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`); diff --git a/frontend/app/admin/clients/[id]/brand/page.tsx b/frontend/app/admin/clients/[id]/brand/page.tsx index 0e4f978..f060ba6 100644 --- a/frontend/app/admin/clients/[id]/brand/page.tsx +++ b/frontend/app/admin/clients/[id]/brand/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { apiFetch } from '../../../../../lib/apiFetch'; import { useParams } from 'next/navigation'; import { useDispatch, useSelector } from 'react-redux'; import { AppDispatch, RootState } from '@/store/store'; @@ -78,7 +79,7 @@ export default function BrandConfigPage() { formData.append('file', file); try { - const res = await fetch(`/api/v1/admin/clients/${clientId}/brand/logo`, { + const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand/logo`, { method: 'POST', body: formData, }); @@ -95,7 +96,7 @@ export default function BrandConfigPage() { const handleDeleteLogo = async (index: number) => { try { - const res = await fetch(`/api/v1/admin/clients/${clientId}/brand/logo/${index}`, { + const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand/logo/${index}`, { method: 'DELETE', }); if (res.ok) { @@ -115,7 +116,7 @@ export default function BrandConfigPage() { formData.append('file', file); try { - const res = await fetch(`/api/v1/admin/clients/${clientId}/brand/guideline`, { + const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand/guideline`, { method: 'POST', body: formData, }); diff --git a/frontend/app/admin/clients/[id]/page.tsx b/frontend/app/admin/clients/[id]/page.tsx index ae02121..2217190 100644 --- a/frontend/app/admin/clients/[id]/page.tsx +++ b/frontend/app/admin/clients/[id]/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { apiFetch } from '../../../../lib/apiFetch'; import { useParams } from 'next/navigation'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; @@ -17,7 +18,7 @@ export default function ClientDetailPage() { useEffect(() => { const fetchClient = async () => { try { - const res = await fetch(`/api/v1/admin/clients/${clientId}`); + const res = await apiFetch(`/api/v1/admin/clients/${clientId}`); if (res.ok) setClient(await res.json()); } finally { setLoading(false); diff --git a/frontend/app/admin/settings/page.tsx b/frontend/app/admin/settings/page.tsx index 856d13e..3e53eb4 100644 --- a/frontend/app/admin/settings/page.tsx +++ b/frontend/app/admin/settings/page.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useEffect, useState, useCallback } from 'react'; +import { apiFetch } from '../../../lib/apiFetch'; import { Settings, Loader2, @@ -78,7 +79,7 @@ export default function SettingsPage() { setLoadingModels(true); setAvailableModels([]); try { - const res = await fetch(`/api/v1/admin/settings/models?provider=${provider}`, { + const res = await apiFetch(`/api/v1/admin/settings/models?provider=${provider}`, { headers: getHeader(), }); if (res.ok) { @@ -103,7 +104,7 @@ export default function SettingsPage() { setLoading(true); setError(null); try { - const res = await fetch('/api/v1/admin/settings', { headers: getHeader() }); + const res = await apiFetch('/api/v1/admin/settings', { headers: getHeader() }); if (res.status === 403) { setError('Super admin access required'); return; @@ -136,7 +137,7 @@ export default function SettingsPage() { return; } - const res = await fetch('/api/v1/admin/settings', { + const res = await apiFetch('/api/v1/admin/settings', { method: 'PUT', headers: { ...getHeader(), 'Content-Type': 'application/json' }, body: JSON.stringify(body), @@ -161,7 +162,7 @@ export default function SettingsPage() { const body: Record = { provider: 'google' }; if (googleKey) body.api_key = googleKey; - const res = await fetch('/api/v1/admin/settings/test-connection', { + const res = await apiFetch('/api/v1/admin/settings/test-connection', { method: 'POST', headers: { ...getHeader(), 'Content-Type': 'application/json' }, body: JSON.stringify(body), diff --git a/frontend/app/admin/storage/page.tsx b/frontend/app/admin/storage/page.tsx index 0ac49ae..b5fc4b9 100644 --- a/frontend/app/admin/storage/page.tsx +++ b/frontend/app/admin/storage/page.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useEffect, useState, useCallback } from 'react'; +import { apiFetch } from '../../../lib/apiFetch'; import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; import { @@ -90,7 +91,7 @@ export default function StoragePage() { // Load client list for super_admin useEffect(() => { if (isSuperAdmin) { - fetch('/api/v1/admin/clients', { headers: getHeader() }) + apiFetch('/api/v1/admin/clients', { headers: getHeader() }) .then((r) => (r.ok ? r.json() : [])) .then((data) => setClients(data)) .catch(() => {}); @@ -104,8 +105,8 @@ export default function StoragePage() { const params = selectedClientId ? `?client_id=${selectedClientId}` : ''; const headers = getHeader(); const [summaryRes, presRes] = await Promise.all([ - fetch(`/api/v1/admin/storage/summary${params}`, { headers }), - fetch(`/api/v1/admin/storage/presentations${params}`, { headers }), + apiFetch(`/api/v1/admin/storage/summary${params}`, { headers }), + apiFetch(`/api/v1/admin/storage/presentations${params}`, { headers }), ]); if (summaryRes.ok) setSummary(await summaryRes.json()); if (presRes.ok) setPresentations(await presRes.json()); @@ -127,7 +128,7 @@ export default function StoragePage() { const handleDelete = async () => { if (!deleteTarget) return; try { - const res = await fetch(`/api/v1/admin/storage/presentations/${deleteTarget.id}`, { + const res = await apiFetch(`/api/v1/admin/storage/presentations/${deleteTarget.id}`, { method: 'DELETE', headers: getHeader(), }); @@ -145,7 +146,7 @@ export default function StoragePage() { const handleBulkDelete = async () => { try { - const res = await fetch('/api/v1/admin/storage/presentations/bulk-delete', { + const res = await apiFetch('/api/v1/admin/storage/presentations/bulk-delete', { method: 'POST', headers: { ...getHeader(), 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: Array.from(selectedIds) }), @@ -167,7 +168,7 @@ export default function StoragePage() { setPurging(true); try { const params = selectedClientId ? `?client_id=${selectedClientId}` : ''; - const res = await fetch(`/api/v1/admin/storage/purge${params}`, { + const res = await apiFetch(`/api/v1/admin/storage/purge${params}`, { method: 'POST', headers: getHeader(), }); diff --git a/frontend/app/admin/templates/[id]/page.tsx b/frontend/app/admin/templates/[id]/page.tsx index 7ef8e16..e1fbf64 100644 --- a/frontend/app/admin/templates/[id]/page.tsx +++ b/frontend/app/admin/templates/[id]/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState, useCallback } from 'react'; +import { apiFetch } from '../../../../lib/apiFetch'; import { useParams } from 'next/navigation'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; @@ -43,7 +44,7 @@ export default function TemplateDetailPage() { const load = useCallback(async () => { setLoading(true); try { - const res = await fetch(`/api/v1/ppt/template-management/get-templates/${templateId}`); + const res = await apiFetch(`/api/v1/ppt/template-management/get-templates/${templateId}`); if (!res.ok) throw new Error('Failed to load template'); const data = await res.json(); diff --git a/frontend/app/admin/templates/page.tsx b/frontend/app/admin/templates/page.tsx index 17cff6b..46f2fc5 100644 --- a/frontend/app/admin/templates/page.tsx +++ b/frontend/app/admin/templates/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState, useCallback } from 'react'; +import { apiFetch } from '../../../lib/apiFetch'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { @@ -35,7 +36,7 @@ export default function TemplatesPage() { const load = useCallback(async () => { setLoading(true); try { - const res = await fetch('/api/v1/ppt/template-management/summary'); + const res = await apiFetch('/api/v1/ppt/template-management/summary'); if (!res.ok) throw new Error('Failed to load templates'); const data = await res.json(); const mapped: TemplateSummary[] = (data.presentations || []).map((item: any) => ({ @@ -58,7 +59,7 @@ export default function TemplatesPage() { if (!deleteTarget) return; setDeleting(true); try { - const res = await fetch(`/api/v1/ppt/template-management/delete-templates/${deleteTarget.id}`, { + const res = await apiFetch(`/api/v1/ppt/template-management/delete-templates/${deleteTarget.id}`, { method: 'DELETE', }); if (!res.ok && res.status !== 204) throw new Error('Failed to delete'); diff --git a/frontend/app/admin/users/[id]/page.tsx b/frontend/app/admin/users/[id]/page.tsx index 9240f47..c54164b 100644 --- a/frontend/app/admin/users/[id]/page.tsx +++ b/frontend/app/admin/users/[id]/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { apiFetch } from '../../../../lib/apiFetch'; import { useParams } from 'next/navigation'; import RoleBadge from '../../components/RoleBadge'; import { Button } from '@/components/ui/button'; @@ -25,7 +26,7 @@ export default function UserDetailPage() { useEffect(() => { const fetchUser = async () => { try { - const res = await fetch(`/api/v1/admin/users/${params.id}`); + const res = await apiFetch(`/api/v1/admin/users/${params.id}`); if (res.ok) setUser(await res.json()); } finally { setLoading(false); diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx index 9ad3a58..33ec50f 100644 --- a/frontend/app/login/page.tsx +++ b/frontend/app/login/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { apiFetch } from '../../lib/apiFetch'; import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; import { useRouter } from 'next/navigation'; @@ -32,7 +33,7 @@ export default function LoginPage() { setLoading(true); try { - const response = await fetch('/api/v1/auth/dev-login', { + const response = await apiFetch('/api/v1/auth/dev-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), diff --git a/frontend/components/AnthropicConfig.tsx b/frontend/components/AnthropicConfig.tsx index 4b61bb6..d746d28 100644 --- a/frontend/components/AnthropicConfig.tsx +++ b/frontend/components/AnthropicConfig.tsx @@ -1,5 +1,6 @@ "use client"; import { useEffect, useState } from "react"; +import { apiFetch } from '../lib/apiFetch'; import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { Button } from "./ui/button"; import { @@ -53,7 +54,7 @@ export default function AnthropicConfig({ setModelsLoading(true); try { - const response = await fetch('/api/v1/ppt/anthropic/models/available', { + const response = await apiFetch('/api/v1/ppt/anthropic/models/available', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/components/GoogleConfig.tsx b/frontend/components/GoogleConfig.tsx index 8d333dd..e888a8b 100644 --- a/frontend/components/GoogleConfig.tsx +++ b/frontend/components/GoogleConfig.tsx @@ -1,5 +1,6 @@ "use client"; import { useEffect, useState } from "react"; +import { apiFetch } from '../lib/apiFetch'; import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { Button } from "./ui/button"; import { @@ -50,7 +51,7 @@ export default function GoogleConfig({ setModelsLoading(true); try { - const response = await fetch('/api/v1/ppt/google/models/available', { + const response = await apiFetch('/api/v1/ppt/google/models/available', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/components/OllamaConfig.tsx b/frontend/components/OllamaConfig.tsx index 74f3098..1bcc0a3 100644 --- a/frontend/components/OllamaConfig.tsx +++ b/frontend/components/OllamaConfig.tsx @@ -1,5 +1,6 @@ "use client"; import { useState, useEffect } from "react"; +import { apiFetch } from '../lib/apiFetch'; import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { Button } from "./ui/button"; import { @@ -41,7 +42,7 @@ export default function OllamaConfig({ const fetchOllamaModels = async () => { try { setOllamaModelsLoading(true); - const response = await fetch('/api/v1/ppt/ollama/models/supported'); + const response = await apiFetch('/api/v1/ppt/ollama/models/supported'); if (response.ok) { const data = await response.json(); diff --git a/frontend/components/OpenAIConfig.tsx b/frontend/components/OpenAIConfig.tsx index 7c465a9..76a8cac 100644 --- a/frontend/components/OpenAIConfig.tsx +++ b/frontend/components/OpenAIConfig.tsx @@ -1,5 +1,6 @@ "use client"; import { useEffect, useState } from "react"; +import { apiFetch } from '../lib/apiFetch'; import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { Button } from "./ui/button"; import { @@ -52,7 +53,7 @@ export default function OpenAIConfig({ setModelsLoading(true); try { - const response = await fetch('/api/v1/ppt/openai/models/available', { + const response = await apiFetch('/api/v1/ppt/openai/models/available', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/lib/apiFetch.ts b/frontend/lib/apiFetch.ts new file mode 100644 index 0000000..c3de143 --- /dev/null +++ b/frontend/lib/apiFetch.ts @@ -0,0 +1,12 @@ +/** + * Wrapper around fetch that prepends the Next.js basePath so API calls + * reach the correct backend when deployed under a sub-path (e.g. /ppt-tool). + * + * Usage: apiFetch('/api/v1/...', options) — identical to fetch(), just works. + */ +const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''; + +export function apiFetch(path: string, init?: RequestInit): Promise { + const url = path.startsWith('/api/') ? `${BASE_PATH}${path}` : path; + return fetch(url, init); +} diff --git a/frontend/store/slices/adminSlice.ts b/frontend/store/slices/adminSlice.ts index 510d3f1..0d3249d 100644 --- a/frontend/store/slices/adminSlice.ts +++ b/frontend/store/slices/adminSlice.ts @@ -1,4 +1,5 @@ import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import { apiFetch } from '../../lib/apiFetch'; export interface AdminUser { id: string; @@ -94,7 +95,7 @@ const initialState: AdminState = { export const fetchUsers = createAsyncThunk( "admin/fetchUsers", async (_, { rejectWithValue }) => { - const res = await fetch("/api/v1/admin/users"); + const res = await apiFetch("/api/v1/admin/users"); if (!res.ok) return rejectWithValue("Failed to fetch users"); return await res.json(); } @@ -103,7 +104,7 @@ export const fetchUsers = createAsyncThunk( export const updateUserRole = createAsyncThunk( "admin/updateUserRole", async ({ userId, role }: { userId: string; role: string }, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/users/${userId}/role?role=${role}`, { method: "PUT" }); + const res = await apiFetch(`/api/v1/admin/users/${userId}/role?role=${role}`, { method: "PUT" }); if (!res.ok) return rejectWithValue("Failed to update role"); return await res.json(); } @@ -112,7 +113,7 @@ export const updateUserRole = createAsyncThunk( export const deactivateUser = createAsyncThunk( "admin/deactivateUser", async (userId: string, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/users/${userId}`, { method: "DELETE" }); + const res = await apiFetch(`/api/v1/admin/users/${userId}`, { method: "DELETE" }); if (!res.ok) return rejectWithValue("Failed to deactivate user"); return userId; } @@ -123,7 +124,7 @@ export const deactivateUser = createAsyncThunk( export const fetchClients = createAsyncThunk( "admin/fetchClients", async (_, { rejectWithValue }) => { - const res = await fetch("/api/v1/admin/clients"); + const res = await apiFetch("/api/v1/admin/clients"); if (!res.ok) return rejectWithValue("Failed to fetch clients"); return await res.json(); } @@ -132,7 +133,7 @@ export const fetchClients = createAsyncThunk( export const createClient = createAsyncThunk( "admin/createClient", async ({ name, review_policy }: { name: string; review_policy?: string }, { rejectWithValue }) => { - const res = await fetch("/api/v1/admin/clients", { + const res = await apiFetch("/api/v1/admin/clients", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, review_policy: review_policy || "self_approve" }), @@ -162,7 +163,7 @@ export const fetchTeams = createAsyncThunk( export const fetchTeamDetail = createAsyncThunk( "admin/fetchTeamDetail", async (teamId: string) => { - const res = await fetch(`/api/v1/admin/teams/${teamId}`); + const res = await apiFetch(`/api/v1/admin/teams/${teamId}`); if (!res.ok) throw new Error("Failed to fetch team"); return await res.json(); } @@ -171,7 +172,7 @@ export const fetchTeamDetail = createAsyncThunk( export const addTeamMember = createAsyncThunk( "admin/addTeamMember", async ({ teamId, userId }: { teamId: string; userId: string }, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/teams/${teamId}/members`, { + const res = await apiFetch(`/api/v1/admin/teams/${teamId}/members`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId }), @@ -187,7 +188,7 @@ export const addTeamMember = createAsyncThunk( export const removeTeamMember = createAsyncThunk( "admin/removeTeamMember", async ({ teamId, userId }: { teamId: string; userId: string }, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/teams/${teamId}/members/${userId}`, { method: "DELETE" }); + const res = await apiFetch(`/api/v1/admin/teams/${teamId}/members/${userId}`, { method: "DELETE" }); if (!res.ok) return rejectWithValue("Failed to remove member"); return { teamId, userId }; } @@ -199,7 +200,7 @@ export const fetchAuditLogs = createAsyncThunk( "admin/fetchAuditLogs", async (params?: Record) => { const query = params ? "?" + new URLSearchParams(params).toString() : ""; - const res = await fetch(`/api/v1/admin/audit-log${query}`); + const res = await apiFetch(`/api/v1/admin/audit-log${query}`); if (!res.ok) throw new Error("Failed to fetch audit logs"); return await res.json(); } @@ -210,7 +211,7 @@ export const fetchAuditLogs = createAsyncThunk( export const fetchBrandConfig = createAsyncThunk( "admin/fetchBrandConfig", async (clientId: string) => { - const res = await fetch(`/api/v1/admin/clients/${clientId}/brand`); + const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand`); if (!res.ok) { if (res.status === 404) return null; throw new Error("Failed to fetch brand config"); @@ -222,7 +223,7 @@ export const fetchBrandConfig = createAsyncThunk( export const updateBrandConfig = createAsyncThunk( "admin/updateBrandConfig", async ({ clientId, data }: { clientId: string; data: Partial }, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/clients/${clientId}/brand`, { + const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), @@ -237,7 +238,7 @@ export const updateBrandConfig = createAsyncThunk( export const fetchMasterDecks = createAsyncThunk( "admin/fetchMasterDecks", async (clientId: string) => { - const res = await fetch(`/api/v1/admin/clients/${clientId}/master-decks`); + const res = await apiFetch(`/api/v1/admin/clients/${clientId}/master-decks`); if (!res.ok) throw new Error("Failed to fetch master decks"); return await res.json(); } @@ -248,7 +249,7 @@ export const uploadMasterDeck = createAsyncThunk( async ({ clientId, file }: { clientId: string; file: File }, { rejectWithValue }) => { const formData = new FormData(); formData.append("file", file); - const res = await fetch(`/api/v1/admin/clients/${clientId}/master-decks`, { + const res = await apiFetch(`/api/v1/admin/clients/${clientId}/master-decks`, { method: "POST", body: formData, }); @@ -263,7 +264,7 @@ export const uploadMasterDeck = createAsyncThunk( export const fetchMasterDeckDetail = createAsyncThunk( "admin/fetchMasterDeckDetail", async (deckId: string) => { - const res = await fetch(`/api/v1/admin/master-decks/${deckId}`); + const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}`); if (!res.ok) throw new Error("Failed to fetch master deck detail"); return await res.json(); } @@ -272,7 +273,7 @@ export const fetchMasterDeckDetail = createAsyncThunk( export const updateMasterDeck = createAsyncThunk( "admin/updateMasterDeck", async ({ deckId, data }: { deckId: string; data: { name?: string; description?: string; is_active?: boolean } }, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/master-decks/${deckId}`, { + const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), @@ -288,7 +289,7 @@ export const updateMasterDeckLayout = createAsyncThunk( { deckId, layoutIndex, data }: { deckId: string; layoutIndex: number; data: { layout_name?: string; layout_type?: string; react_code?: string } }, { rejectWithValue } ) => { - const res = await fetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, { + const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), @@ -302,7 +303,7 @@ export const reparseMasterDeck = createAsyncThunk( "admin/reparseMasterDeck", async ({ deckId, parseMode }: { deckId: string; parseMode?: string }, { rejectWithValue }) => { const params = parseMode ? `?parse_mode=${parseMode}` : ""; - const res = await fetch(`/api/v1/admin/master-decks/${deckId}/reparse${params}`, { method: "POST" }); + const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/reparse${params}`, { method: "POST" }); if (!res.ok) { const data = await res.json().catch(() => ({})); return rejectWithValue(data.detail || "Failed to trigger reparse"); @@ -314,7 +315,7 @@ export const reparseMasterDeck = createAsyncThunk( export const deleteMasterDeck = createAsyncThunk( "admin/deleteMasterDeck", async (deckId: string, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/master-decks/${deckId}`, { method: "DELETE" }); + const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}`, { method: "DELETE" }); if (!res.ok) return rejectWithValue("Failed to delete master deck"); return deckId; } @@ -323,7 +324,7 @@ export const deleteMasterDeck = createAsyncThunk( export const deleteMasterDeckLayout = createAsyncThunk( "admin/deleteMasterDeckLayout", async ({ deckId, layoutIndex }: { deckId: string; layoutIndex: number }, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, { + const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, { method: "DELETE", }); if (!res.ok) return rejectWithValue("Failed to delete layout"); @@ -334,7 +335,7 @@ export const deleteMasterDeckLayout = createAsyncThunk( export const bulkDeleteMasterDeckLayouts = createAsyncThunk( "admin/bulkDeleteMasterDeckLayouts", async ({ deckId, indices }: { deckId: string; indices: number[] }, { rejectWithValue }) => { - const res = await fetch(`/api/v1/admin/master-decks/${deckId}/layouts/bulk-delete`, { + const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/layouts/bulk-delete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ indices }),