291 lines
8.5 KiB
TypeScript
291 lines
8.5 KiB
TypeScript
/*
|
|||
|
|
Copyright 2024-2026 ChatterMate
|
||
|
|
|
||
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
you may not use this file except in compliance with the License.
|
||
|
|
You may obtain a copy of the License at
|
||
|
|
|
||
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
|
||
|
|
Unless required by applicable law or agreed to in writing, software
|
||
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
|
See the License for the specific language governing permissions and
|
||
|
|
limitations under the License.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import api from './api'
|
||
|
|
import type {
|
||
|
|
FaqGenerationJob,
|
||
|
|
FaqItem,
|
||
|
|
FaqImportMode,
|
||
|
|
FaqListResponse,
|
||
|
|
FaqSeoFields,
|
||
|
|
FaqStatus,
|
||
|
|
GenerateEstimate,
|
||
|
|
HelpCenterDomain,
|
||
|
|
HelpCenterSettings,
|
||
|
|
HelpCenterSettingsUpdate,
|
||
|
|
} from '@/types/faq'
|
||
|
|
|
||
|
|
function errorMessage(error: any, fallback: string): Error {
|
||
|
|
return new Error(error.response?.data?.detail || fallback)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Backend bulk endpoints cap faq_ids at 200 (MAX_BULK_IDS in schemas/faq.py);
|
||
|
|
// split larger selections so a select-all over a big FAQ set still works.
|
||
|
|
const MAX_BULK_IDS = 200
|
||
|
|
|
||
|
|
function chunk<T>(items: T[], size: number): T[][] {
|
||
|
|
const batches: T[][] = []
|
||
|
|
for (let i = 0; i < items.length; i += size) batches.push(items.slice(i, i + size))
|
||
|
|
return batches
|
||
|
|
}
|
||
|
|
|
||
|
|
export const faqService = {
|
||
|
|
async getFaqs(params: { status?: FaqStatus; category?: string; q?: string; page?: number; page_size?: number } = {}): Promise<FaqListResponse> {
|
||
|
|
try {
|
||
|
|
const response = await api.get('/help-center/faqs', { params: { page_size: 200, ...params } })
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to load FAQs')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async uploadImage(file: File): Promise<string> {
|
||
|
|
try {
|
||
|
|
const formData = new FormData()
|
||
|
|
formData.append('file', file)
|
||
|
|
const response = await api.post('/help-center/faqs/image', formData, {
|
||
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||
|
|
})
|
||
|
|
return response.data.url as string
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to upload image')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async createFaq(
|
||
|
|
payload: {
|
||
|
|
question: string
|
||
|
|
answer: string
|
||
|
|
category?: string
|
||
|
|
status?: FaqStatus
|
||
|
|
} & Partial<FaqSeoFields>,
|
||
|
|
): Promise<FaqItem> {
|
||
|
|
try {
|
||
|
|
const response = await api.post('/help-center/faqs', payload)
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to create FAQ')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async updateFaq(
|
||
|
|
id: string,
|
||
|
|
payload: Partial<
|
||
|
|
Pick<FaqItem, 'question' | 'answer' | 'category' | 'status'> & FaqSeoFields
|
||
|
|
>,
|
||
|
|
): Promise<FaqItem> {
|
||
|
|
try {
|
||
|
|
const response = await api.put(`/help-center/faqs/${id}`, payload)
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to update FAQ')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async deleteFaq(id: string): Promise<void> {
|
||
|
|
try {
|
||
|
|
await api.delete(`/help-center/faqs/${id}`)
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to delete FAQ')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async setStatus(ids: string[], status: FaqStatus): Promise<number> {
|
||
|
|
try {
|
||
|
|
let updated = 0
|
||
|
|
for (const batch of chunk(ids, MAX_BULK_IDS)) {
|
||
|
|
const response = await api.post('/help-center/faqs/bulk-status', { faq_ids: batch, status })
|
||
|
|
updated += response.data.updated
|
||
|
|
}
|
||
|
|
return updated
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to update FAQ status')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async bulkDelete(ids: string[]): Promise<number> {
|
||
|
|
try {
|
||
|
|
let deleted = 0
|
||
|
|
for (const batch of chunk(ids, MAX_BULK_IDS)) {
|
||
|
|
const response = await api.post('/help-center/faqs/bulk-delete', { faq_ids: batch })
|
||
|
|
deleted += response.data.deleted
|
||
|
|
}
|
||
|
|
return deleted
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to delete FAQs')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async getGenerateEstimate(includePages = true): Promise<GenerateEstimate> {
|
||
|
|
try {
|
||
|
|
const response = await api.get('/help-center/generate/estimate', {
|
||
|
|
params: { include_pages: includePages },
|
||
|
|
})
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to load generation estimate')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async startGeneration(knowledgeIds?: number[]): Promise<FaqGenerationJob> {
|
||
|
|
try {
|
||
|
|
const response = await api.post(
|
||
|
|
'/help-center/generate',
|
||
|
|
knowledgeIds?.length ? { knowledge_ids: knowledgeIds } : {},
|
||
|
|
)
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to start generation')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async importFaq(
|
||
|
|
url: string,
|
||
|
|
mode: FaqImportMode = 'qa',
|
||
|
|
preserveUrls = false,
|
||
|
|
): Promise<FaqGenerationJob> {
|
||
|
|
try {
|
||
|
|
const response = await api.post('/help-center/import', {
|
||
|
|
url,
|
||
|
|
mode,
|
||
|
|
preserve_urls: preserveUrls,
|
||
|
|
})
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to start import')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async importPdf(file: File): Promise<FaqGenerationJob> {
|
||
|
|
try {
|
||
|
|
const formData = new FormData()
|
||
|
|
formData.append('file', file)
|
||
|
|
const response = await api.post('/help-center/import/pdf', formData, {
|
||
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||
|
|
})
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to start PDF import')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async getJob(active = true): Promise<FaqGenerationJob | null> {
|
||
|
|
try {
|
||
|
|
const response = await api.get('/help-center/jobs', { params: { active } })
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to load job status')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async getSettings(): Promise<HelpCenterSettings> {
|
||
|
|
try {
|
||
|
|
const response = await api.get('/help-center/settings')
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to load help center settings')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async updateSettings(payload: HelpCenterSettingsUpdate): Promise<HelpCenterSettings> {
|
||
|
|
try {
|
||
|
|
const response = await api.put('/help-center/settings', payload)
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to save help center settings')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async uploadLogo(file: File): Promise<HelpCenterSettings> {
|
||
|
|
try {
|
||
|
|
const formData = new FormData()
|
||
|
|
formData.append('file', file)
|
||
|
|
const response = await api.post('/help-center/logo', formData, {
|
||
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||
|
|
})
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to upload logo')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async removeLogo(): Promise<HelpCenterSettings> {
|
||
|
|
try {
|
||
|
|
const response = await api.delete('/help-center/logo')
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to remove logo')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async uploadFavicon(file: File): Promise<HelpCenterSettings> {
|
||
|
|
try {
|
||
|
|
const formData = new FormData()
|
||
|
|
formData.append('file', file)
|
||
|
|
const response = await api.post('/help-center/favicon', formData, {
|
||
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||
|
|
})
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to upload favicon')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async removeFavicon(): Promise<HelpCenterSettings> {
|
||
|
|
try {
|
||
|
|
const response = await api.delete('/help-center/favicon')
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to remove favicon')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async setDomain(domain: string): Promise<HelpCenterDomain> {
|
||
|
|
try {
|
||
|
|
const response = await api.post('/help-center/domain', { domain })
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to set custom domain')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async removeDomain(): Promise<HelpCenterDomain> {
|
||
|
|
try {
|
||
|
|
const response = await api.delete('/help-center/domain')
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to remove custom domain')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async verifyDomain(): Promise<HelpCenterDomain> {
|
||
|
|
try {
|
||
|
|
const response = await api.post('/help-center/domain/verify')
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to verify domain')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
async getDomainStatus(): Promise<HelpCenterDomain> {
|
||
|
|
try {
|
||
|
|
const response = await api.get('/help-center/domain/status')
|
||
|
|
return response.data
|
||
|
|
} catch (error: any) {
|
||
|
|
throw errorMessage(error, 'Failed to load domain status')
|
||
|
|
}
|
||
|
|
},
|
||
|
|
}
|