Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions controllers/message_answer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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())
Expand Down
1 change: 1 addition & 0 deletions object/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
26 changes: 23 additions & 3 deletions web/src/ChatBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -41,6 +42,7 @@ class ChatBox extends React.Component {
isLoadingTTS: false,
isVoiceInput: false,
rerenderErrorMessage: false,
promptModalVisible: false,
};
this.synth = window.speechSynthesis;
this.cursorPosition = undefined;
Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -331,7 +343,6 @@ class ChatBox extends React.Component {
<Layout style={{display: "flex", width: "100%", height: "100%", borderRadius: "6px"}}>
<Card style={{display: "flex", width: "100%", height: "100%", flexDirection: "column", position: "relative", padding: "24px"}}>
{messages.length === 0 && <WelcomeHeader store={this.props.store} />}

<MessageList
ref={this.messageListRef}
messages={messages}
Expand Down Expand Up @@ -364,13 +375,22 @@ class ChatBox extends React.Component {
disableInput={this.props.disableInput}
messageError={this.props.messageError}
onCancelMessage={this.props.onCancelMessage}
onPromptClick={this.handlePromptClick}
promptValue={this.props.promptValue}
onVoiceInputStart={this.startVoiceInput}
onVoiceInputEnd={this.stopVoiceInput}
isVoiceInput={this.state.isVoiceInput}
/>
)}
</Card>

<PromptModal
visible={this.state.promptModalVisible}
initialValue={this.props.promptValue}
onSave={this.handlePromptSave}
onCancel={this.handlePromptCancel}
loading={this.props.loading}
disabled={this.props.disableInput}
/>
{messages.length === 0 ? (
<ChatPrompts
sendMessage={this.props.sendMessage}
Expand Down
23 changes: 23 additions & 0 deletions web/src/ChatPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class ChatPage extends BaseListPage {
defaultStore: null,
filteredStores: [],
paneCount: 1,
chatPrompt: "",
});

this.fetch();
Expand Down Expand Up @@ -162,9 +163,26 @@ class ChatPage extends BaseListPage {
userAgent: this.props.account.education,
messageCount: 0,
needTitle: true,
prompt: "",
};
}
updateChatPrompt = (prompt) => {
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 {
Expand All @@ -182,6 +200,7 @@ class ChatPage extends BaseListPage {
isAlerted: false,
isRegenerated: isRegenerated,
fileName: fileName,
customPrompt: this.state.chatPrompt || "",
};
}

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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}
/>

</div>
)}
</div>
Expand Down
20 changes: 18 additions & 2 deletions web/src/chat/ChatInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,6 +27,8 @@ const ChatInput = ({
onFileChange,
onChange,
onSend,
onPromptClick,
promptValue,
loading,
disableInput,
messageError,
Expand Down Expand Up @@ -106,7 +108,7 @@ const ChatInput = ({
return (
<div style={{position: "absolute", bottom: 0, left: 0, right: 0, padding: "16px 24px", zIndex: 1}}>
<UploadFileArea onFileChange={handleInputChange} />
<div style={{maxWidth: "700px", margin: "0 auto"}}>
<div style={{maxWidth: "700px", margin: "0 auto", display: "flex", alignItems: "center", gap: "8px"}}>
{files.length > 0 && (
<div style={{marginBottom: "12px", marginLeft: "12px", marginRight: "12px"}}>
<ChatFileInput
Expand Down Expand Up @@ -157,6 +159,20 @@ const ChatInput = ({
},
} : {})}
/>

<Button
icon={<SettingOutlined />}
onClick={onPromptClick}
disabled={disableInput}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "36px",
}}
>
{promptValue ? i18next.t("chat:Edit Prompt") : i18next.t("chat:Set Prompt")}
</Button>
</div>
</div>
);
Expand Down
84 changes: 84 additions & 0 deletions web/src/chat/PromptModal.js
Original file line number Diff line number Diff line change
@@ -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 (
<Modal
title={i18next.t("chat:Custom Prompt")}
open={visible}
onCancel={handleCancel}
onOk={handleSave}
okText={i18next.t("general:Save")}
cancelText={i18next.t("general:Cancel")}
width={520}
confirmLoading={loading}
bodyStyle={{
padding: "20px 24px",
}}
>
<textarea
value={editingPrompt}
onChange={(e) => setEditingPrompt(e.target.value)}
placeholder={i18next.t("chat:Set a custom prompt for this conversation")}
style={{
width: "100%",
minHeight: "120px",
padding: "12px",
borderRadius: "6px",
border: "1px solid #d9d9d9",
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
fontSize: "14px",
lineHeight: "1.5",
color: "rgba(0, 0, 0, 0.85)",
resize: "vertical",
boxShadow: "inset 0 1px 2px rgba(0,0,0,0.03)",
transition: "all 0.3s",
outline: "none",
boxSizing: "border-box",
}}
disabled={disabled || loading}
rows={6}
/>
</Modal>
);
};

export default PromptModal;