diff --git a/controllers/message_answer.go b/controllers/message_answer.go index c3bc0798b..9b7943bd9 100644 --- a/controllers/message_answer.go +++ b/controllers/message_answer.go @@ -210,6 +210,11 @@ func (c *ApiController) GetMessageAnswer() { c.ResponseErrorStream(message, err.Error()) return } + + customPrompt := "" + if questionMessage != nil { + customPrompt = questionMessage.CustomPrompt + } var modelResult *model.ModelResult if agentClients != nil { messages := &model.AgentMessages{ @@ -220,10 +225,11 @@ func (c *ApiController) GetMessageAnswer() { AgentClients: agentClients, AgentMessages: messages, } - modelResult, err = model.QueryTextWithTools(modelProviderObj, question, writer, history, store.Prompt, knowledge, agentInfo) + modelResult, err = model.QueryTextWithTools(modelProviderObj, question, writer, history, store.Prompt+customPrompt, knowledge, agentInfo) } else { - modelResult, err = modelProviderObj.QueryText(question, writer, history, store.Prompt, knowledge, nil) + modelResult, err = modelProviderObj.QueryText(question, writer, history, store.Prompt+customPrompt, knowledge, nil) } + if err != nil { if strings.Contains(err.Error(), "write tcp") { c.ResponseError(err.Error()) diff --git a/object/message.go b/object/message.go index b3acac2df..9802f6bbc 100644 --- a/object/message.go +++ b/object/message.go @@ -51,6 +51,7 @@ type Message struct { ErrorText string `xorm:"mediumtext" json:"errorText"` FileName string `xorm:"varchar(100)" json:"fileName"` Comment string `xorm:"mediumtext" json:"comment"` + CustomPrompt string `xorm:"mediumtext" json:"customPrompt"` TokenCount int `json:"tokenCount"` TextTokenCount int `json:"textTokenCount"` Price float64 `json:"price"` diff --git a/web/src/ChatBox.js b/web/src/ChatBox.js index 0441d4c68..8f2933a15 100644 --- a/web/src/ChatBox.js +++ b/web/src/ChatBox.js @@ -22,6 +22,7 @@ import ChatPrompts from "./ChatPrompts"; import MessageList from "./chat/MessageList"; import ChatInput from "./chat/ChatInput"; import WelcomeHeader from "./chat/WelcomeHeader"; +import PromptModal from "./chat/PromptModal"; import * as MessageBackend from "./backend/MessageBackend"; import TtsHelper from "./TextToSpeech"; import SpeechToTextHelper from "./SpeechToText"; @@ -41,6 +42,7 @@ class ChatBox extends React.Component { isLoadingTTS: false, isVoiceInput: false, rerenderErrorMessage: false, + promptModalVisible: false, }; this.synth = window.speechSynthesis; this.cursorPosition = undefined; @@ -49,7 +51,18 @@ class ChatBox extends React.Component { this.ttsHelper = new TtsHelper(this); this.sttHelper = new SpeechToTextHelper(this); } + handlePromptClick = () => { + this.setState({promptModalVisible: true}); + }; + + handlePromptCancel = () => { + this.setState({promptModalVisible: false}); + }; + handlePromptSave = (prompt) => { + this.props.onPromptChange(prompt); + this.setState({promptModalVisible: false}); + }; componentDidMount() { window.addEventListener("beforeunload", () => { this.synth.cancel(); @@ -58,7 +71,6 @@ class ChatBox extends React.Component { } componentDidUpdate(prevProps, prevState, snapshot) { - // clear old status when the name(chat) changes if (prevProps.name !== this.props.name) { inputStore.set(prevProps.name, this.state.value); this.clearOldStatus(); @@ -331,7 +343,6 @@ class ChatBox extends React.Component { {messages.length === 0 && } - )} - + {messages.length === 0 ? ( { + if (!this.state.chat) {return;} + this.setState({chatPrompt: prompt}); + + const chatId = this.state.chat.owner + "/" + this.state.chat.name; + localStorage.setItem(`chatPrompt_${chatId}`, prompt); + + Setting.showMessage("success", i18next.t("general:Successfully updated")); + }; + loadChatPrompt = (chat) => { + if (!chat) {return;} + + const chatId = chat.owner + "/" + chat.name; + const savedPrompt = localStorage.getItem(`chatPrompt_${chatId}`) || ""; + this.setState({chatPrompt: savedPrompt}); + }; newMessage(text, fileName, isHidden, isRegenerated) { const randomName = Setting.getRandomName(); return { @@ -182,6 +200,7 @@ class ChatPage extends BaseListPage { isAlerted: false, isRegenerated: isRegenerated, fileName: fileName, + customPrompt: this.state.chatPrompt || "", }; } @@ -271,6 +290,7 @@ class ChatPage extends BaseListPage { } getMessages(chat) { + this.loadChatPrompt(chat); MessageBackend.getChatMessages("admin", chat.name) .then((res) => { if (this.getMessageAnswerFromURL(res.data)) { @@ -705,7 +725,10 @@ class ChatPage extends BaseListPage { name={this.state.chat?.name} displayName={this.state.chat?.displayName} store={this.state.chat ? this.state.stores?.find(store => store.name === this.state.chat.store) : this.state.stores?.find(store => store.isDefault === true)} + promptValue={this.state.chatPrompt} + onPromptChange={this.updateChatPrompt} /> + )} diff --git a/web/src/chat/ChatInput.js b/web/src/chat/ChatInput.js index 1204db794..1de670fc6 100644 --- a/web/src/chat/ChatInput.js +++ b/web/src/chat/ChatInput.js @@ -15,7 +15,7 @@ import React from "react"; import {Button} from "antd"; import {Sender} from "@ant-design/x"; -import {LinkOutlined} from "@ant-design/icons"; +import {LinkOutlined, SettingOutlined} from "@ant-design/icons"; import ChatFileInput from "./ChatFileInput"; import UploadFileArea from "./UploadFileArea"; import i18next from "i18next"; @@ -27,6 +27,8 @@ const ChatInput = ({ onFileChange, onChange, onSend, + onPromptClick, + promptValue, loading, disableInput, messageError, @@ -106,7 +108,7 @@ const ChatInput = ({ return (
-
+
{files.length > 0 && (
+ +
); diff --git a/web/src/chat/PromptModal.js b/web/src/chat/PromptModal.js new file mode 100644 index 000000000..415635f25 --- /dev/null +++ b/web/src/chat/PromptModal.js @@ -0,0 +1,84 @@ +// Copyright 2025 The Casibase Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, {useEffect, useState} from "react"; +import {Modal} from "antd"; +import i18next from "i18next"; + +const PromptModal = ({ + visible, + initialValue, + onSave, + onCancel, + loading = false, + disabled = false, +}) => { + const [editingPrompt, setEditingPrompt] = useState(initialValue || ""); + + useEffect(() => { + setEditingPrompt(initialValue || ""); + }, [initialValue]); + + const handleSave = () => { + onSave(editingPrompt); + }; + + const handleCancel = () => { + + setEditingPrompt(initialValue || ""); + onCancel(); + }; + + return ( + +