feat(knowledge): add FAQ Excel import export
This commit is contained in:
+53
-1
@@ -1,5 +1,5 @@
|
||||
import { readSession } from "@/lib/auth"
|
||||
import { request } from "@/lib/api/client"
|
||||
import { request, requestBlob } from "@/lib/api/client"
|
||||
import { createWebSocketBaseUrl } from "@/lib/api/websocket"
|
||||
import { translateCurrentMessage } from "@/i18n/messages"
|
||||
|
||||
@@ -1524,6 +1524,22 @@ export type UpdateKnowledgeFAQPayload = CreateKnowledgeFAQPayload & {
|
||||
id: number
|
||||
}
|
||||
|
||||
export type KnowledgeFAQImportMode = "append" | "overwrite"
|
||||
|
||||
export type KnowledgeFAQImportError = {
|
||||
row: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export type KnowledgeFAQImportResult = {
|
||||
total: number
|
||||
created: number
|
||||
updated: number
|
||||
skipped: number
|
||||
failed: number
|
||||
errors: KnowledgeFAQImportError[]
|
||||
}
|
||||
|
||||
export function fetchKnowledgeBases(
|
||||
query?: Record<string, string | number | undefined>
|
||||
) {
|
||||
@@ -1652,6 +1668,42 @@ export function deleteKnowledgeFAQ(id: number) {
|
||||
})
|
||||
}
|
||||
|
||||
function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement("a")
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export async function downloadKnowledgeFAQImportTemplate() {
|
||||
const result = await requestBlob("/api/dashboard/knowledge-faq/import_template")
|
||||
downloadBlob(result.blob, result.filename || "knowledge-faq-import-template.xlsx")
|
||||
}
|
||||
|
||||
export async function exportKnowledgeFAQs(knowledgeBaseId: number) {
|
||||
const result = await requestBlob(
|
||||
`/api/dashboard/knowledge-faq/export${toQueryString({ knowledgeBaseId })}`
|
||||
)
|
||||
downloadBlob(result.blob, result.filename || `knowledge-faq-${knowledgeBaseId}.xlsx`)
|
||||
}
|
||||
|
||||
export function importKnowledgeFAQs(payload: {
|
||||
knowledgeBaseId: number
|
||||
mode: KnowledgeFAQImportMode
|
||||
file: File
|
||||
}) {
|
||||
const formData = new FormData()
|
||||
formData.set("knowledgeBaseId", String(payload.knowledgeBaseId))
|
||||
formData.set("mode", payload.mode)
|
||||
formData.set("file", payload.file)
|
||||
return request<KnowledgeFAQImportResult>("/api/dashboard/knowledge-faq/import", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
|
||||
export function buildKnowledgeFAQIndex(faqId: number) {
|
||||
return request<void>("/api/dashboard/knowledge-retrieve/build", {
|
||||
method: "POST",
|
||||
|
||||
+65
-9
@@ -18,6 +18,11 @@ type RequestOptions = RequestInit & {
|
||||
onResponse?: (response: Response) => void
|
||||
}
|
||||
|
||||
export type BlobResponse = {
|
||||
blob: Blob
|
||||
filename: string
|
||||
}
|
||||
|
||||
async function parseResult<T>(response: Response) {
|
||||
const payload = (await response.json()) as JsonResult<T>
|
||||
if (!response.ok || !payload.success) {
|
||||
@@ -31,13 +36,7 @@ async function parseResult<T>(response: Response) {
|
||||
return payload.data
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<T> {
|
||||
const { headers, skipAuth, baseUrl, onResponse, ...rest } = options
|
||||
delete (rest as RequestOptions).baseUrl
|
||||
delete (rest as RequestOptions).onResponse
|
||||
function buildRequestHeaders(headers: HeadersInit | undefined, skipAuth?: boolean, body?: BodyInit | null) {
|
||||
const session = readSession()
|
||||
const authHeaders = new Headers(headers)
|
||||
|
||||
@@ -46,14 +45,37 @@ export async function request<T>(
|
||||
}
|
||||
if (
|
||||
!authHeaders.has("Content-Type") &&
|
||||
rest.body &&
|
||||
!(typeof FormData !== "undefined" && rest.body instanceof FormData)
|
||||
body &&
|
||||
!(typeof FormData !== "undefined" && body instanceof FormData)
|
||||
) {
|
||||
authHeaders.set("Content-Type", "application/json")
|
||||
}
|
||||
const locale = readStoredLocale()
|
||||
authHeaders.set("Accept-Language", locale)
|
||||
authHeaders.set("X-Locale", locale)
|
||||
return authHeaders
|
||||
}
|
||||
|
||||
function parseFilename(contentDisposition: string | null) {
|
||||
if (!contentDisposition) {
|
||||
return ""
|
||||
}
|
||||
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
|
||||
if (utf8Match?.[1]) {
|
||||
return decodeURIComponent(utf8Match[1])
|
||||
}
|
||||
const match = contentDisposition.match(/filename="?([^";]+)"?/i)
|
||||
return match?.[1] ? decodeURIComponent(match[1]) : ""
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<T> {
|
||||
const { headers, skipAuth, baseUrl, onResponse, ...rest } = options
|
||||
delete (rest as RequestOptions).baseUrl
|
||||
delete (rest as RequestOptions).onResponse
|
||||
const authHeaders = buildRequestHeaders(headers, skipAuth, rest.body)
|
||||
|
||||
const requestBaseUrl = baseUrl !== undefined ? baseUrl : API_BASE_URL
|
||||
const response = await fetch(`${requestBaseUrl}${path}`, {
|
||||
@@ -65,3 +87,37 @@ export async function request<T>(
|
||||
|
||||
return parseResult<T>(response)
|
||||
}
|
||||
|
||||
export async function requestBlob(
|
||||
path: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<BlobResponse> {
|
||||
const { headers, skipAuth, baseUrl, onResponse, ...rest } = options
|
||||
delete (rest as RequestOptions).baseUrl
|
||||
delete (rest as RequestOptions).onResponse
|
||||
const authHeaders = buildRequestHeaders(headers, skipAuth, rest.body)
|
||||
const requestBaseUrl = baseUrl !== undefined ? baseUrl : API_BASE_URL
|
||||
const response = await fetch(`${requestBaseUrl}${path}`, {
|
||||
...rest,
|
||||
headers: authHeaders,
|
||||
cache: "no-store",
|
||||
})
|
||||
onResponse?.(response)
|
||||
|
||||
const contentType = response.headers.get("Content-Type") ?? ""
|
||||
if (contentType.includes("application/json")) {
|
||||
try {
|
||||
await parseResult<never>(response)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(translateCurrentMessage("api.requestFailed"))
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText || translateCurrentMessage("api.requestFailed"))
|
||||
}
|
||||
return {
|
||||
blob: await response.blob(),
|
||||
filename: parseFilename(response.headers.get("Content-Disposition")),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user