@websolutespa/payload-plugin-bowl-llm
Version:
LLM plugin for Bowl PayloadCms plugin
566 lines (565 loc) • 28.8 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { MinimalTemplate } from '@payloadcms/next/templates';
import { Button, Form, Modal, SelectInput, toast, useConfig, useModal, useTranslation, XIcon } from '@payloadcms/ui';
import React, { useCallback, useRef, useState } from 'react';
import { LlmKnowledgeBaseCreationMode } from '../../api/consts';
import './GenerateKbModal.scss';
const baseClass = 'generate-kb-options';
export const BtnGenerateKbVectorDb = (props)=>{
const { config } = useConfig();
const modalKey = 'generate-kb-modal';
const bulkUploadModalKey = 'bulk-upload-modal';
const { openModal, isModalOpen, closeAllModals } = useModal();
const [loading, setLoading] = React.useState(false);
// tools
const [toolOptions, setToolOptions] = React.useState([]);
const [selectedTool, setSelectedTool] = React.useState('');
// generation mode
const [generationModeOptions, setGenerationModeOptions] = React.useState([]);
const [selectedGenerationMode, setSelectedGenerationMode] = React.useState('');
// llm apps
const [llmApps, setllmApps] = useState([]);
const [selectedLlmApp, setSelectedLlmApp] = React.useState('');
// bulk upload state
const [bulkLlmApps, setBulkLlmApps] = useState([]);
const [bulkSelectedApp, setBulkSelectedApp] = useState('');
const [bulkToolOptions, setBulkToolOptions] = useState([]);
const [bulkSelectedTool, setBulkSelectedTool] = useState('');
const [bulkFiles, setBulkFiles] = useState([]);
const [bulkUploading, setBulkUploading] = useState(false);
const fileInputRef = useRef(null);
const { t } = useTranslation();
const intervalRef = useRef(null);
const { routes: { api: apiRoute } = {} } = config;
React.useEffect(()=>{
const fetchOptions = async ()=>{
try {
if (!selectedLlmApp) {
return;
}
const url = `${process.env.PAYLOAD_PUBLIC_BASE_PATH || ''}/api/llm/appTools?llmApp=${selectedLlmApp}&onlyActive`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
// tools select options
const toolsOpts = await response.json();
setToolOptions(toolsOpts);
if (toolsOpts.length > 0) {
setSelectedTool('');
}
// generation mode select options
const generationModeOpts = [
{
label: t('general:generateKbAppOnly'),
value: LlmKnowledgeBaseCreationMode.appOnly
}
];
if (toolsOpts.length > 0) {
generationModeOpts.unshift({
label: t('general:generateKbAppAndTools'),
value: LlmKnowledgeBaseCreationMode.appAndTools
});
generationModeOpts.push({
label: t('general:generateKbToolsOnly'),
value: LlmKnowledgeBaseCreationMode.toolsOnly
});
generationModeOpts.push({
label: t('general:generateKbSpecificTool'),
value: LlmKnowledgeBaseCreationMode.specificTool
});
}
setGenerationModeOptions(generationModeOpts);
setSelectedGenerationMode(generationModeOpts[0].value);
} else {
setToolOptions([
{
label: (await response.json()).message,
value: ''
}
]);
}
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchOptions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
selectedLlmApp
]);
// Fetch tool options for bulk upload when app changes
React.useEffect(()=>{
const fetchBulkToolOptions = async ()=>{
try {
if (!bulkSelectedApp) {
setBulkToolOptions([]);
return;
}
const url = `${process.env.PAYLOAD_PUBLIC_BASE_PATH || ''}/api/llm/appTools?llmApp=${bulkSelectedApp}&onlyActive`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const toolsOpts = await response.json();
setBulkToolOptions([
{
label: t('general:kbFileNoToolOption'),
value: ''
},
...toolsOpts
]);
}
} catch (error) {
console.error('Error fetching bulk tool options:', error);
}
};
fetchBulkToolOptions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
bulkSelectedApp
]);
const pollKbGeneration = useCallback(async (traceId)=>{
try {
console.log('polling job ...', traceId);
const httpResponse = await fetch(`${apiRoute}/llm/kb/${traceId}/poll`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (httpResponse.ok) {
const response = await httpResponse.json();
console.log(response);
if (!response.processing) {
if (response.success) {
toast.success(t('general:generateKbSuccess'));
} else {
const errors = response.errors || [];
const toastMsg = /*#__PURE__*/ _jsxs("div", {
children: [
/*#__PURE__*/ _jsx("div", {
children: t('general:generateKbError')
}),
/*#__PURE__*/ _jsx("ul", {
children: errors.map((error, idx)=>/*#__PURE__*/ _jsx("li", {
children: /*#__PURE__*/ _jsxs("div", {
children: [
error.job,
": ",
error.taskStatus,
" - ",
error.error
]
})
}, idx))
})
]
});
toast.error(toastMsg, {}); // { autoClose: false });
}
setLoading(false);
clearInterval(intervalRef.current);
}
} else {
throw new Error(`Response status: ${httpResponse.status}`);
}
} catch (error) {
clearInterval(intervalRef.current);
console.error(`${t('general:generateKbError')}: ${error}`);
toast.error(`${t('general:generateKbError')}: ${error}`, {}); // { autoClose: false });
setLoading(false);
}
}, [
apiRoute,
t
]);
const generateKb = async ()=>{
setLoading(true);
try {
const httpResponse = await fetch(`${apiRoute}/llm/kb`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
referrer: '',
body: JSON.stringify({
appId: selectedLlmApp,
mode: selectedGenerationMode,
toolFunctionId: selectedTool
})
});
if (httpResponse.ok) {
const response = await httpResponse.json();
const intervalId = setInterval(()=>{
pollKbGeneration(response.traceId);
}, 2000);
intervalRef.current = intervalId;
} else {
throw new Error(`Response status: ${httpResponse.status}`);
}
} catch (error) {
console.error(`${t('general:generateKbError')}: ${error}`);
toast.error(`${t('general:generateKbError')}: ${error}`, {}); // { autoClose: false });
setLoading(false);
}
};
const onSubmit = async ()=>{
try {
// generate kb with the selected options
generateKb();
closeAllModals();
} catch (error) {
console.error(t('general:generateKbError') + ': ', error);
toast.error(t('general:generateKbError'));
}
};
const handleClick = async ()=>{
try {
// get user apps for which it's possible to generate kb
const httpResponse = await fetch(`${apiRoute}/llm/apps`, {
method: 'GET'
});
const response = await httpResponse.json();
if (httpResponse.ok) {
const apps = response;
setllmApps(apps.map((app)=>({
value: app.id,
label: app.name
})));
openModal(modalKey);
} else {
toast.error(response.message);
return;
}
} catch (error) {
console.error(t('general:generateKbError') + ': ', error);
toast.error(t('general:generateKbError'));
}
};
// --- Bulk Upload handlers ---
const handleBulkUploadClick = async ()=>{
try {
const httpResponse = await fetch(`${apiRoute}/llm/apps`, {
method: 'GET'
});
const response = await httpResponse.json();
if (httpResponse.ok) {
const apps = response;
setBulkLlmApps(apps.map((app)=>({
value: app.id,
label: app.name
})));
setBulkSelectedApp('');
setBulkSelectedTool('');
setBulkFiles([]);
openModal(bulkUploadModalKey);
} else {
toast.error(response.message);
}
} catch (error) {
console.error(`${t('general:bulkUploadError')}: ${error}`);
toast.error(t('general:bulkUploadError'));
}
};
const handleFileSelect = (e)=>{
if (e.target.files) {
const newFiles = Array.from(e.target.files);
setBulkFiles((prev)=>[
...prev,
...newFiles
]);
}
// reset input so the same file can be selected again
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const removeFile = (index)=>{
setBulkFiles((prev)=>prev.filter((_, i)=>i !== index));
};
const handleBulkUploadSubmit = async ()=>{
if (!bulkSelectedApp || bulkFiles.length === 0) return;
setBulkUploading(true);
try {
const formData = new FormData();
formData.append('llmApp', bulkSelectedApp);
if (bulkSelectedTool) {
formData.append('appTool', bulkSelectedTool);
}
bulkFiles.forEach((file)=>{
formData.append('files', file, file.name);
});
const httpResponse = await fetch(`${apiRoute}/llm/kb/bulk-upload`, {
method: 'POST',
body: formData
});
if (httpResponse.ok || httpResponse.status === 207) {
const result = await httpResponse.json();
if (result.errorCount === 0) {
toast.success(`${t('general:bulkUploadSuccess')} (${result.successCount} file)`);
} else {
const toastMsg = /*#__PURE__*/ _jsxs("div", {
children: [
/*#__PURE__*/ _jsx("div", {
children: t('general:bulkUploadPartial')
}),
/*#__PURE__*/ _jsxs("div", {
children: [
"✓ ",
result.successCount,
" | ✗ ",
result.errorCount
]
}),
/*#__PURE__*/ _jsx("ul", {
children: result.results.filter((r)=>!r.success).map((r, i)=>/*#__PURE__*/ _jsxs("li", {
children: [
r.filename,
": ",
r.error
]
}, i))
})
]
});
toast.warning(toastMsg, {}); // { autoClose: false });
}
closeAllModals();
// reload the page to show new files
window.location.reload();
} else {
throw new Error(`Response status: ${httpResponse.status}`);
}
} catch (error) {
console.error(`${t('general:bulkUploadError')}: ${error}`);
toast.error(`${t('general:bulkUploadError')}: ${error}`, {}); // { autoClose: false });
} finally{
setBulkUploading(false);
}
};
const handleSelectChange = (setter)=>{
return (e)=>setter(e.value);
};
return /*#__PURE__*/ _jsxs("div", {
children: [
/*#__PURE__*/ _jsx("div", {
className: `${baseClass}__bulk-upload-header`,
children: /*#__PURE__*/ _jsx(Button, {
onClick: handleBulkUploadClick,
buttonStyle: "secondary",
disabled: bulkUploading,
children: bulkUploading ? t('general:bulkUploadUploading') : t('general:bulkUpload')
})
}),
/*#__PURE__*/ _jsx(Button, {
onClick: handleClick,
disabled: loading,
children: loading ? t('general:generatingKb') : t('general:generateKb')
}),
/*#__PURE__*/ _jsx(Modal, {
slug: modalKey,
className: `${baseClass}__modal`,
children: isModalOpen(modalKey) && /*#__PURE__*/ _jsxs(MinimalTemplate, {
children: [
/*#__PURE__*/ _jsxs("header", {
className: `${baseClass}__header`,
children: [
/*#__PURE__*/ _jsx("h3", {
children: t('general:generateKbModalTitle')
}),
/*#__PURE__*/ _jsx(Button, {
buttonStyle: "none",
onClick: closeAllModals,
children: /*#__PURE__*/ _jsx(XIcon, {})
})
]
}),
/*#__PURE__*/ _jsx("main", {
className: `${baseClass}__main`,
children: /*#__PURE__*/ _jsxs(Form, {
onSubmit: onSubmit,
children: [
/*#__PURE__*/ _jsxs("div", {
className: "render-fields",
children: [
/*#__PURE__*/ _jsx(SelectInput, {
label: t('general:selectLlmApp'),
name: "llmApp",
path: 'llmApp',
options: llmApps,
value: selectedLlmApp,
onChange: (option)=>setSelectedLlmApp(String(Array.isArray(option) ? option.map((x)=>x.value).join(', ') : option.value))
}),
/*#__PURE__*/ _jsx(SelectInput, {
label: t('general:selectKbGenerationMode'),
name: "generationMode",
path: 'generationMode',
options: generationModeOptions,
value: selectedGenerationMode,
onChange: (option)=>setSelectedGenerationMode(String(Array.isArray(option) ? option.map((x)=>x.value).join(', ') : option.value))
}),
selectedGenerationMode === LlmKnowledgeBaseCreationMode.specificTool && /*#__PURE__*/ _jsx(SelectInput, {
label: t('general:selectTool'),
name: "tool",
path: 'tool',
options: toolOptions,
value: selectedTool,
onChange: (option)=>setSelectedTool(String(Array.isArray(option) ? option.map((x)=>x.value).join(', ') : option.value))
})
]
}),
/*#__PURE__*/ _jsx("div", {
className: `${baseClass}__foot`,
children: /*#__PURE__*/ _jsx(Button, {
type: "submit",
children: t('general:submit')
})
})
]
})
})
]
})
}),
/*#__PURE__*/ _jsx(Modal, {
slug: bulkUploadModalKey,
className: `${baseClass}__modal`,
children: isModalOpen(bulkUploadModalKey) && /*#__PURE__*/ _jsxs(MinimalTemplate, {
children: [
/*#__PURE__*/ _jsxs("header", {
className: `${baseClass}__header`,
children: [
/*#__PURE__*/ _jsx("h3", {
children: t('general:bulkUploadTitle')
}),
/*#__PURE__*/ _jsx(Button, {
buttonStyle: "none",
onClick: closeAllModals,
children: /*#__PURE__*/ _jsx(XIcon, {})
})
]
}),
/*#__PURE__*/ _jsxs("main", {
className: `${baseClass}__main`,
children: [
/*#__PURE__*/ _jsxs("div", {
className: "render-fields",
children: [
/*#__PURE__*/ _jsx(SelectInput, {
label: t('general:selectLlmApp'),
name: "bulkLlmApp",
path: 'bulkLlmApp',
options: bulkLlmApps,
value: bulkSelectedApp,
onChange: (option)=>setBulkSelectedApp(String(Array.isArray(option) ? option.map((x)=>x.value).join(', ') : option.value))
}),
bulkToolOptions.length > 0 && /*#__PURE__*/ _jsx(SelectInput, {
label: t('general:selectTool'),
name: "bulkTool",
path: 'bulkTool',
options: bulkToolOptions,
value: bulkSelectedTool,
onChange: (option)=>setBulkSelectedTool(String(Array.isArray(option) ? option.map((x)=>x.value).join(', ') : option.value))
}),
/*#__PURE__*/ _jsxs("div", {
className: `${baseClass}__dropzone`,
children: [
/*#__PURE__*/ _jsx("label", {
className: `${baseClass}__dropzone-label`,
children: t('general:selectFiles')
}),
/*#__PURE__*/ _jsxs("div", {
className: `${baseClass}__dropzone-area`,
onClick: ()=>fileInputRef.current?.click(),
onDragOver: (e)=>{
e.preventDefault();
e.currentTarget.classList.add(`${baseClass}__dropzone-area--active`);
},
onDragLeave: (e)=>{
e.currentTarget.classList.remove(`${baseClass}__dropzone-area--active`);
},
onDrop: (e)=>{
e.preventDefault();
e.currentTarget.classList.remove(`${baseClass}__dropzone-area--active`);
if (e.dataTransfer.files) {
const newFiles = Array.from(e.dataTransfer.files);
setBulkFiles((prev)=>[
...prev,
...newFiles
]);
}
},
children: [
/*#__PURE__*/ _jsx("span", {
className: `${baseClass}__dropzone-icon`,
children: "📁"
}),
/*#__PURE__*/ _jsx("span", {
className: `${baseClass}__dropzone-text`,
children: bulkFiles.length === 0 ? t('general:noFilesSelected') : `${bulkFiles.length} ${t('general:filesSelected')}`
})
]
}),
/*#__PURE__*/ _jsx("input", {
ref: fileInputRef,
type: "file",
multiple: true,
style: {
display: 'none'
},
onChange: handleFileSelect
})
]
}),
bulkFiles.length > 0 && /*#__PURE__*/ _jsx("div", {
className: `${baseClass}__file-list`,
children: bulkFiles.map((file, index)=>/*#__PURE__*/ _jsxs("div", {
className: `${baseClass}__file-item`,
children: [
/*#__PURE__*/ _jsx("span", {
className: `${baseClass}__file-name`,
children: file.name
}),
/*#__PURE__*/ _jsxs("span", {
className: `${baseClass}__file-size`,
children: [
(file.size / 1024).toFixed(1),
" KB"
]
}),
/*#__PURE__*/ _jsx(Button, {
buttonStyle: "none",
className: `${baseClass}__file-remove`,
onClick: ()=>removeFile(index),
children: /*#__PURE__*/ _jsx(XIcon, {})
})
]
}, `${file.name}-${index}`))
})
]
}),
/*#__PURE__*/ _jsx("div", {
className: `${baseClass}__foot`,
children: /*#__PURE__*/ _jsx(Button, {
onClick: handleBulkUploadSubmit,
disabled: !bulkSelectedApp || bulkFiles.length === 0 || bulkUploading,
children: bulkUploading ? t('general:bulkUploadUploading') : `${t('general:bulkUpload')} (${bulkFiles.length})`
})
})
]
})
]
})
})
]
});
};
//# sourceMappingURL=BtnGenerateKbVectorDb.js.map