diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 376a100b..626f743a 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -28,6 +28,9 @@ export class ChatFlow implements IChatFlow { @Column({ nullable: true, type: 'text' }) apiConfig?: string + @Column({ nullable: true, type: 'text' }) + starterPrompt?: string + @Column({ nullable: true, type: 'text' }) analytic?: string diff --git a/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts new file mode 100644 index 00000000..1f7a3c54 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddStarterPrompt1700565042576 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'starterPrompt') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`starterPrompt\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`starterPrompt\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a95..f38a9cfe 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddStarterPrompt1700565042576 ] diff --git a/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts new file mode 100644 index 00000000..ac2694b9 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddStarterPrompt1700565042576 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "starterPrompt" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "starterPrompt";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b..e9018265 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddStarterPrompt1700565042576 ] diff --git a/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts new file mode 100644 index 00000000..3d180797 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddStarterPrompt1700565042576 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "starterPrompt" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "starterPrompt";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc40..414d755e 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddStarterPrompt1700565042576 ] diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 307bd0bd..9224b78f 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,8 +1,8 @@ // assets -import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons' +import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } from '@tabler/icons' // constant -const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } +const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -11,6 +11,13 @@ const settings = { title: '', type: 'group', children: [ + { + id: 'conversationStarters', + title: 'Set Starter Prompts', + type: 'item', + url: '', + icon: icons.IconPictureInPictureOff + }, { id: 'viewMessages', title: 'View Messages', diff --git a/packages/ui/src/ui-component/cards/StarterConversationCard.css b/packages/ui/src/ui-component/cards/StarterConversationCard.css new file mode 100644 index 00000000..8eb3b0ad --- /dev/null +++ b/packages/ui/src/ui-component/cards/StarterConversationCard.css @@ -0,0 +1,16 @@ +.button-container { + display: flex; + overflow-x: auto; + -webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */ + scrollbar-width: none; /* For Firefox */ +} + +/*.button-container::-webkit-scrollbar {*/ +/* display: none; !* For Chrome, Safari, and Opera *!*/ +/*}*/ + +.button { + flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ + margin: 5px; /* Adjust as needed for spacing between buttons */ + /* Add additional button styling here */ +} diff --git a/packages/ui/src/ui-component/cards/StarterConversationCard.js b/packages/ui/src/ui-component/cards/StarterConversationCard.js index 946c4c11..aa50f266 100644 --- a/packages/ui/src/ui-component/cards/StarterConversationCard.js +++ b/packages/ui/src/ui-component/cards/StarterConversationCard.js @@ -1,49 +1,24 @@ -import Chip from '@mui/material/Chip' import Box from '@mui/material/Box' import PropTypes from 'prop-types' -import { MenuItem, Select } from '@mui/material' +import { Button } from '@mui/material' +import './StarterConversationCard.css' -const StarterConversationCard = ({ isGrid, chipsData, onChipClick }) => { - if (isGrid) { - const chipStyle = { - margin: '5px', - width: 'calc(50% - 10px)' - } - - return ( - - {chipsData.map((chipLabel, index) => ( - onChipClick(chipLabel)} /> - ))} - - ) - } else { - return ( - - ) - } +const StarterConversationCard = ({ isGrid, starterPrompts, onPromptClick }) => { + return ( + + {starterPrompts.map((sp, index) => ( + + ))} + + ) } StarterConversationCard.propTypes = { isGrid: PropTypes.bool, - chipsData: PropTypes.arrayOf(PropTypes.string), - onChipClick: PropTypes.func + starterPrompts: PropTypes.arrayOf(PropTypes.string), + onPromptClick: PropTypes.func } export default StarterConversationCard diff --git a/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js b/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js new file mode 100644 index 00000000..1139f21e --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js @@ -0,0 +1,188 @@ +import { createPortal } from 'react-dom' +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions' + +// material-ui +import { Button, IconButton, Dialog, DialogContent, OutlinedInput, DialogTitle, DialogActions, Box, List, Divider } from '@mui/material' +import { IconX, IconTrash, IconPlus } from '@tabler/icons' + +// Project import +import { StyledButton } from 'ui-component/button/StyledButton' + +// store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import useNotifier from 'utils/useNotifier' + +// API +import chatflowsApi from 'api/chatflows' + +const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [inputFields, setInputFields] = useState([ + { + prompt: '' + } + ]) + + const addInputField = () => { + setInputFields([ + ...inputFields, + { + prompt: '' + } + ]) + } + const removeInputFields = (index) => { + const rows = [...inputFields] + rows.splice(index, 1) + setInputFields(rows) + } + + const handleChange = (index, evnt) => { + const { name, value } = evnt.target + const list = [...inputFields] + list[index][name] = value + setInputFields(list) + } + + const onSave = async () => { + try { + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + starterPrompt: JSON.stringify(inputFields) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Conversation Starter Prompts Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + onCancel() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Conversation Starter Prompts: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.starterPrompt) { + try { + setInputFields(JSON.parse(dialogProps.chatflow.starterPrompt)) + } catch (e) { + setInputFields([ + { + prompt: '' + } + ]) + console.error(e) + } + } + + return () => {} + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + Set Conversation Starter Prompts + + + + + {inputFields.map((data, index) => { + return ( +
+ + handleChange(index, e)} + value={data.prompt} + name='prompt' + /> + + + {inputFields.length !== 1 && ( + + + + )} + + + {index === inputFields.length - 1 && ( + + + + )} + +
+ ) + })} +
+
+
+ + + + Cancel + + + Save + + +
+ ) : null + + return createPortal(component, portalElement) +} + +ConversationStarterDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default ConversationStarterDialog diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index 56365ba8..d931ee26 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -16,6 +16,7 @@ import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog' import APICodeDialog from 'views/chatflows/APICodeDialog' import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog' import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog' +import ConversationStarterDialog from 'ui-component/dialog/ConversationStarterDialog' // API import chatflowsApi from 'api/chatflows' @@ -45,6 +46,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [apiDialogProps, setAPIDialogProps] = useState({}) const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false) const [analyseDialogProps, setAnalyseDialogProps] = useState({}) + const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false) + const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({}) const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({}) @@ -56,6 +59,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl if (setting === 'deleteChatflow') { handleDeleteFlow() + } else if (setting === 'conversationStarters') { + setConversationStartersDialogProps({ + title: 'Set Conversation Starters - ' + chatflow.name, + chatflow: chatflow + }) + setConversationStartersDialogOpen(true) } else if (setting === 'analyseChatflow') { setAnalyseDialogProps({ title: 'Analyse Chatflow', @@ -376,6 +385,11 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl /> setAPIDialogOpen(false)} /> setAnalyseDialogOpen(false)} /> + setConversationStartersDialogOpen(false)} + /> { const [limitMax, setLimitMax] = useState(apiConfig?.rateLimit?.limitMax ?? '') const [limitDuration, setLimitDuration] = useState(apiConfig?.rateLimit?.limitDuration ?? '') const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? '') - const [prompt1, setPrompt1] = useState(apiConfig?.prompt?.prompt1 ?? '') - const [prompt2, setPrompt2] = useState(apiConfig?.prompt?.prompt2 ?? '') - const [prompt3, setPrompt3] = useState(apiConfig?.prompt?.prompt3 ?? '') - const [prompt4, setPrompt4] = useState(apiConfig?.prompt?.prompt4 ?? '') const formatObj = () => { const obj = { @@ -119,7 +111,7 @@ const Configuration = () => { return (
- {fieldLabel && {fieldLabel}} + {fieldLabel} { return ( <> - - } aria-controls='panel1a-content' id='panel1a-header'> - - Rate Limit{' '} - Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' - } - /> - - - - - {/*Rate Limit*/} - {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number')} - {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number')} - {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} - - - + {/*Rate Limit*/} + + Rate Limit{' '} + Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' + } + /> + + {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number')} + {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number')} + {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} - - } aria-controls='panel2a-content' id='panel2a-header'> - Conversation Starters - - - - {textField(prompt1, 'prompt1', 'Starter Prompts', 'string')} - {textField(prompt2, 'prompt2', '', 'string')} - {textField(prompt3, 'prompt3', '', 'string')} - {textField(prompt4, 'prompt4', '', 'string')} - - - onSave()}> Save Changes diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index d047a3aa..8e9b753b 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -57,6 +57,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const inputRef = useRef(null) const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow) + + const [starterPrompts, setStarterPrompts] = useState([]) const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) @@ -104,14 +107,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, 100) } - const handlePromptClick = async (prompt) => { + const handlePromptClick = async (prompt, e) => { setUserInput(prompt) - await handleSubmit() + await handleSubmit(e) } // Handle form submission const handleSubmit = async (e) => { - if (e) e.preventDefault() + e.preventDefault() if (userInput.trim() === '') { return @@ -202,10 +205,18 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (getIsChatflowStreamingApi.data) { setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [getIsChatflowStreamingApi.data]) + useEffect(() => { + if (getChatflowConfig.data) { + if (getChatflowConfig.data?.starterPrompt && JSON.parse(getChatflowConfig.data?.starterPrompt)) { + setStarterPrompts(JSON.parse(getChatflowConfig.data?.starterPrompt)) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatflowConfig.data]) + // Auto scroll chat to bottom useEffect(() => { scrollToBottom() @@ -224,6 +235,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (open && chatflowid) { getChatmessageApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid) + getChatflowConfig.request(chatflowid) scrollToBottom() socket = socketIOClient(baseURL) @@ -377,8 +389,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
{messages && messages.length === 1 && ( )}