@websolutespa/payload-plugin-bowl-llm
Version:
LLM plugin for Bowl PayloadCms plugin
352 lines (351 loc) • 14.1 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Button, Form, FormSubmit, TextareaField, toast, useConfig, useLocale, useTranslation } from '@payloadcms/ui';
import { getRouteResolver } from '@websolutespa/payload-utils';
import React, { useMemo, useRef, useState } from 'react';
import { isChunkText, isChunkTextOrString } from '../../api/types';
import { LLM_DEFAULT_MAX_FILE_SIZE, LLM_DEFAULT_MIME_TYPES } from '../../types';
import './Llm.scss';
import { LlmChatCompletion } from './LlmChatCompletion';
import { LlmThread } from './LlmThread';
import { fileToResizedBase64 } from './upload';
export const LlmTestApp_ = ({ app })=>{
const { t } = useTranslation();
const { config } = useConfig();
const { localization, routes, serverURL } = config;
const routeResolver = getRouteResolver(config);
const locale = useLocale();
const [response, setResponse] = useState();
const [messages, setMessages] = useState([]);
const [completion, setCompletion] = useState();
const [upload, setUpload] = useState();
const ref = useRef(null);
const initialState = useMemo(()=>{
const initialState = {
message: {
initialValue: undefined,
valid: true,
value: undefined
}
};
return initialState;
}, []);
const getUserMessage = (prompt, uploaded)=>{
if (uploaded) {
const chunkItems = [
{
type: 'text',
text: prompt
},
{
type: uploaded.type,
url: uploaded.url
}
];
const userMessage = {
chunks: chunkItems,
content: chunkItems,
role: 'user'
};
return userMessage;
} else {
const userMessage = {
chunks: [
{
type: 'text',
text: prompt
}
],
content: prompt,
role: 'user'
};
return userMessage;
}
};
const sendMessage = (prompt, uploaded)=>{
const userMessage = getUserMessage(prompt, uploaded);
// console.log('LlmTestApp.sendMessage', userMessage);
const completion = [
...messages,
userMessage
];
setUpload(undefined);
setMessages(completion);
setCompletion(completion);
};
const getUploaded = async (upload)=>{
if (upload) {
const url = routeResolver.api(`/llmUserUpload/upload?locale=${locale}`);
try {
const data = new FormData();
data.set('appKey', app.settings.credentials.appKey);
data.set('apiKey', app.settings.credentials.apiKey);
data.set('file', upload.file, upload.file.name);
const response = await fetch(url, {
method: 'POST',
body: data
});
if (response.ok) {
const uploaded = await response.json();
return uploaded && {
...uploaded,
type: uploaded.mimeType.indexOf('image') === 0 ? 'image' : 'file'
};
} else {
console.error('useLlm.api.fetch', response, url);
return undefined;
}
} catch (error) {
console.error('useLlm.api.fetch', error, url);
return undefined;
}
}
return undefined;
};
const onSubmit = async (fields, data)=>{
if (!data.message) {
return;
}
if (fields.message) {
fields.message.value = '';
}
let uploaded;
if (upload) {
uploaded = await getUploaded(upload);
if (!uploaded) {
return;
}
}
sendMessage(data.message, uploaded);
};
const onSuccess = async (json)=>{
return initialState;
};
const onError = (error)=>{
console.log('LlmTestApp.onError', error);
toast.error(`An error occurred: ${error.message}.`);
};
const onMessage = (response)=>{
if (ref.current) {
ref.current.scrollTo({
top: ref.current.scrollHeight,
behavior: 'smooth'
});
ref.current.scrollIntoView({
behavior: 'smooth'
});
}
};
const onEnd = (response)=>{
// console.log('LlmTestApp.onEnd', response);
const chunks = [
...response.chunks
];
const assistantMessage = {
chunks,
content: '',
role: 'assistant'
};
// filtering history
const validChunks = chunks.filter((x)=>![
'header',
'info',
'end',
'formRequest',
'formRecap',
'formRecapSuccess',
'formRecapError'
].includes(x.type));
const parsedChunks = [];
validChunks.reduce((p, c)=>{
const firstChunk = p.length > 1 && p[p.length - 2];
const secondChunk = p.length > 1 && p[p.length - 1];
if (firstChunk && secondChunk && isChunkTextOrString(c) && isChunkTextOrString(firstChunk) && isChunkTextOrString(secondChunk)) {
const text = (isChunkText(c) ? c.text : c.content) || '';
if (isChunkText(secondChunk)) {
secondChunk.text += text;
} else {
secondChunk.content += text;
}
} else {
p.push({
...c
});
}
return p;
}, parsedChunks);
assistantMessage.content = parsedChunks.map((x)=>JSON.stringify(x)).join(',') + ',';
const responseMessages = [
...messages,
assistantMessage
];
setResponse(response);
setMessages(responseMessages);
setCompletion(undefined);
};
const hasUpload = app?.contents.enableUpload === true;
const addFile = async (event)=>{
const files = event.target?.files;
try {
if (files && files.length > 0) {
const file = files[0];
if (!file) {
console.log('LlmTestApp.uploadFile.error', 'file not found');
return;
}
const maxFileSize = app.contents.maxFileSize || LLM_DEFAULT_MAX_FILE_SIZE;
if (file.size > maxFileSize) {
alert(`Max file size ${maxFileSize}`);
return;
}
const base64 = file.type.indexOf('image') === 0 ? await fileToResizedBase64(file) : undefined;
event.target.value = null;
const upload = {
file,
base64
};
setUpload(upload);
console.log('LlmTestApp.uploadFile.success', upload);
} else {
console.log('LlmTestApp.uploadFile.error', 'file not found');
}
} catch (error) {
console.log('LlmTestApp.uploadFile.error', 'cannot parse file');
}
};
const removeFile = ()=>{
setUpload(undefined);
};
const systemMessage = app.settings.llmConfig.prompt.prompt?.systemMessage || app.settings.llmConfig.prompt.systemMessage;
const sampleInputTexts = app.contents.sampleInputTexts?.filter((x)=>x.sampleInputText);
const layoutBuilder = app.contents.layoutBuilder?.filter((x)=>x.title && x.message);
const mimeTypes = app.contents.mimeTypes || LLM_DEFAULT_MIME_TYPES;
return /*#__PURE__*/ _jsxs(Form, {
initialState: initialState,
disableSuccessStatus: true,
onSubmit: onSubmit,
onSuccess: onSuccess,
children: [
/*#__PURE__*/ _jsxs("div", {
className: "llm__head",
children: [
/*#__PURE__*/ _jsx("h1", {
children: app.name
}),
systemMessage && /*#__PURE__*/ _jsxs("div", {
className: "llm__row",
children: [
/*#__PURE__*/ _jsx("h4", {
children: "System Message"
}),
/*#__PURE__*/ _jsx("pre", {
children: systemMessage
})
]
}),
sampleInputTexts && sampleInputTexts.length > 0 && /*#__PURE__*/ _jsx("div", {
className: "llm__row llm__flex",
children: sampleInputTexts.map((x, i)=>/*#__PURE__*/ _jsx("button", {
type: "button",
className: "btn btn--style-secondary btn--size-small",
onClick: ()=>sendMessage(x.sampleInputText),
children: x.sampleInputText
}, `sampleInputTexts_${i}`))
}),
layoutBuilder && layoutBuilder.length > 0 && /*#__PURE__*/ _jsx("div", {
className: "llm__row llm__flex",
children: layoutBuilder.map((x, i)=>/*#__PURE__*/ _jsx("button", {
type: "button",
className: "btn btn--style-secondary btn--size-small",
onClick: ()=>sendMessage(x.message),
children: x.title
}, `sampleInputTexts_${i}`))
})
]
}),
/*#__PURE__*/ _jsxs("div", {
ref: ref,
className: "llm__main",
children: [
/*#__PURE__*/ _jsx(LlmThread, {
messages: messages
}),
completion && /*#__PURE__*/ _jsx(LlmChatCompletion, {
apiKey: app.settings.credentials.apiKey,
appKey: app.settings.credentials.appKey,
completion: completion,
test: false,
threadId: response?.threadId,
onMessage: onMessage,
onEnd: onEnd,
onError: onError
})
]
}),
/*#__PURE__*/ _jsxs("div", {
className: "llm__foot",
children: [
/*#__PURE__*/ _jsx(TextareaField, {
field: {
name: 'message',
label: t('general:message'),
required: true,
admin: {
placeholder: 'Type you message'
}
},
path: "message"
}),
/*#__PURE__*/ _jsx(FormSubmit, {
type: "submit",
disabled: typeof completion !== 'undefined',
children: t('general:send')
}),
hasUpload && /*#__PURE__*/ _jsxs(_Fragment, {
children: [
/*#__PURE__*/ _jsxs("div", {
className: "llm__upload",
children: [
/*#__PURE__*/ _jsx(Button, {
buttonStyle: "secondary",
className: "llm__upload-cta",
children: t('general:upload')
}),
/*#__PURE__*/ _jsx("input", {
className: "llm__upload-input",
type: "file",
id: "file-input",
accept: mimeTypes,
onChange: addFile
})
]
}),
upload && /*#__PURE__*/ _jsxs(Button, {
buttonStyle: "secondary",
size: "small",
className: "llm__upload-file",
onClick: ()=>removeFile(),
children: [
/*#__PURE__*/ _jsx("span", {
className: "llm__upload-filename",
children: upload.file.name
}),
/*#__PURE__*/ _jsx("span", {
children: "✖️"
}),
/*#__PURE__*/ _jsx("img", {
className: "llm__upload-image",
src: upload.base64,
alt: ""
})
]
})
]
})
]
})
]
});
};
export const LlmTestApp = /*#__PURE__*/ React.memo(LlmTestApp_);
//# sourceMappingURL=LlmTestApp.js.map