UNPKG

@websolutespa/payload-plugin-bowl-llm

Version:

LLM plugin for Bowl PayloadCms plugin

352 lines (351 loc) 14.1 kB
'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