mirror of
https://github.com/usememos/memos.git
synced 2025-03-20 20:50:07 +01:00
feat: support messages to ask AI (#1380)
This commit is contained in:
parent
8b20cb9fd2
commit
573f07ec82
@ -1,5 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
type OpenAICompletionRequest struct {
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
}
|
|
@ -24,7 +24,7 @@ type ChatCompletionResponse struct {
|
|||||||
Choices []ChatCompletionChoice `json:"choices"`
|
Choices []ChatCompletionChoice `json:"choices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostChatCompletion(prompt string, apiKey string, apiHost string) (string, error) {
|
func PostChatCompletion(messages []ChatCompletionMessage, apiKey string, apiHost string) (string, error) {
|
||||||
if apiHost == "" {
|
if apiHost == "" {
|
||||||
apiHost = "https://api.openai.com"
|
apiHost = "https://api.openai.com"
|
||||||
}
|
}
|
||||||
@ -34,8 +34,12 @@ func PostChatCompletion(prompt string, apiKey string, apiHost string) (string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]interface{}{
|
values := map[string]interface{}{
|
||||||
"model": "gpt-3.5-turbo",
|
"model": "gpt-3.5-turbo",
|
||||||
"messages": []map[string]string{{"role": "user", "content": prompt}},
|
"messages": messages,
|
||||||
|
"max_tokens": 2000,
|
||||||
|
"temperature": 0,
|
||||||
|
"frequency_penalty": 0.0,
|
||||||
|
"presence_penalty": 0.0,
|
||||||
}
|
}
|
||||||
jsonValue, err := json.Marshal(values)
|
jsonValue, err := json.Marshal(values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -31,15 +31,15 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusBadRequest, "OpenAI API key not set")
|
return echo.NewHTTPError(http.StatusBadRequest, "OpenAI API key not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
completionRequest := api.OpenAICompletionRequest{}
|
messages := []openai.ChatCompletionMessage{}
|
||||||
if err := json.NewDecoder(c.Request().Body).Decode(&completionRequest); err != nil {
|
if err := json.NewDecoder(c.Request().Body).Decode(&messages); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post chat completion request").SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post chat completion request").SetInternal(err)
|
||||||
}
|
}
|
||||||
if completionRequest.Prompt == "" {
|
if len(messages) == 0 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Prompt is required")
|
return echo.NewHTTPError(http.StatusBadRequest, "No messages provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := openai.PostChatCompletion(completionRequest.Prompt, openAIConfig.Key, openAIConfig.Host)
|
result, err := openai.PostChatCompletion(messages, openAIConfig.Key, openAIConfig.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to post chat completion").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to post chat completion").SetInternal(err)
|
||||||
}
|
}
|
||||||
@ -47,42 +47,6 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
|
|||||||
return c.JSON(http.StatusOK, composeResponse(result))
|
return c.JSON(http.StatusOK, composeResponse(result))
|
||||||
})
|
})
|
||||||
|
|
||||||
g.POST("/openai/text-completion", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
openAIConfigSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
|
||||||
Name: api.SystemSettingOpenAIConfigName,
|
|
||||||
})
|
|
||||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find openai key").SetInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
openAIConfig := api.OpenAIConfig{}
|
|
||||||
if openAIConfigSetting != nil {
|
|
||||||
err = json.Unmarshal([]byte(openAIConfigSetting.Value), &openAIConfig)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal openai system setting value").SetInternal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if openAIConfig.Key == "" {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "OpenAI API key not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
textCompletion := api.OpenAICompletionRequest{}
|
|
||||||
if err := json.NewDecoder(c.Request().Body).Decode(&textCompletion); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post text completion request").SetInternal(err)
|
|
||||||
}
|
|
||||||
if textCompletion.Prompt == "" {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Prompt is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := openai.PostTextCompletion(textCompletion.Prompt, openAIConfig.Key, openAIConfig.Host)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to post text completion").SetInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, composeResponse(result))
|
|
||||||
})
|
|
||||||
|
|
||||||
g.GET("/openai/enabled", func(c echo.Context) error {
|
g.GET("/openai/enabled", func(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
openAIConfigSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
openAIConfigSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
"react-router-dom": "^6.8.2",
|
"react-router-dom": "^6.8.2",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4",
|
||||||
|
"zustand": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash-es": "^4.17.5",
|
"@types/lodash-es": "^4.17.5",
|
||||||
|
@ -4,24 +4,21 @@ import { toast } from "react-hot-toast";
|
|||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { marked } from "../labs/marked";
|
import { marked } from "../labs/marked";
|
||||||
|
import { useMessageStore } from "../store/zustand/message";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import showSettingDialog from "./SettingDialog";
|
import showSettingDialog from "./SettingDialog";
|
||||||
|
|
||||||
type Props = DialogProps;
|
type Props = DialogProps;
|
||||||
|
|
||||||
interface History {
|
|
||||||
question: string;
|
|
||||||
answer: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AskAIDialog: React.FC<Props> = (props: Props) => {
|
const AskAIDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { destroy, hide } = props;
|
const { destroy, hide } = props;
|
||||||
const fetchingState = useLoading(false);
|
const fetchingState = useLoading(false);
|
||||||
const [historyList, setHistoryList] = useState<History[]>([]);
|
const messageStore = useMessageStore();
|
||||||
const [isEnabled, setIsEnabled] = useState<boolean>(true);
|
const [isEnabled, setIsEnabled] = useState<boolean>(true);
|
||||||
const [isInIME, setIsInIME] = useState(false);
|
const [isInIME, setIsInIME] = useState(false);
|
||||||
const [question, setQuestion] = useState<string>("");
|
const [question, setQuestion] = useState<string>("");
|
||||||
|
const messageList = messageStore.messageList;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.checkOpenAIEnabled().then(({ data }) => {
|
api.checkOpenAIEnabled().then(({ data }) => {
|
||||||
@ -47,10 +44,18 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendQuestionButtonClick = async () => {
|
const handleSendQuestionButtonClick = async () => {
|
||||||
|
if (!question) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetchingState.setLoading();
|
fetchingState.setLoading();
|
||||||
setQuestion("");
|
setQuestion("");
|
||||||
|
messageStore.addMessage({
|
||||||
|
role: "user",
|
||||||
|
content: question,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await askQuestion(question);
|
await fetchChatCompletion();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(error.response.data.error);
|
toast.error(error.response.data.error);
|
||||||
@ -58,21 +63,15 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
|
|||||||
fetchingState.setFinish();
|
fetchingState.setFinish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const askQuestion = async (question: string) => {
|
const fetchChatCompletion = async () => {
|
||||||
if (question === "") {
|
const messageList = messageStore.getState().messageList;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { data: answer },
|
data: { data: answer },
|
||||||
} = await api.postChatCompletion(question);
|
} = await api.postChatCompletion(messageList);
|
||||||
setHistoryList([
|
messageStore.addMessage({
|
||||||
{
|
role: "assistant",
|
||||||
question,
|
content: answer.replace(/^\n\n/, ""),
|
||||||
answer: answer.replace(/^\n\n/, ""),
|
});
|
||||||
},
|
|
||||||
...historyList,
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -87,7 +86,36 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container !w-112 max-w-full">
|
<div className="dialog-content-container !w-112 max-w-full">
|
||||||
<div className="w-full relative">
|
{messageList.map((message, index) => (
|
||||||
|
<div key={index} className="w-full flex flex-col justify-start items-start mt-4 space-y-2">
|
||||||
|
{message.role === "user" ? (
|
||||||
|
<div className="w-full flex flex-row justify-end items-start pl-6">
|
||||||
|
<span className="word-break shadow rounded-lg rounded-tr-none px-3 py-2 opacity-80 bg-gray-100 dark:bg-zinc-700">
|
||||||
|
{message.content}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-row justify-start items-start pr-8 space-x-2">
|
||||||
|
<Icon.Bot className="mt-2 flex-shrink-0 mr-1 w-6 h-auto opacity-80" />
|
||||||
|
<div className="memo-content-wrapper !w-auto flex flex-col justify-start items-start shadow rounded-lg rounded-tl-none px-3 py-2 bg-gray-100 dark:bg-zinc-700">
|
||||||
|
<div className="memo-content-text">{marked(message.content)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{fetchingState.isLoading && (
|
||||||
|
<p className="w-full py-2 mt-4 flex flex-row justify-center items-center">
|
||||||
|
<Icon.Loader className="w-5 h-auto animate-spin" />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!isEnabled && (
|
||||||
|
<div className="w-full flex flex-col justify-center items-center mt-4 space-y-2">
|
||||||
|
<p>You have not set up your OpenAI API key.</p>
|
||||||
|
<Button onClick={() => handleGotoSystemSetting()}>Go to settings</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="w-full relative mt-4">
|
||||||
<Textarea
|
<Textarea
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="Ask anything…"
|
placeholder="Ask anything…"
|
||||||
@ -104,32 +132,6 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
|
|||||||
onClick={handleSendQuestionButtonClick}
|
onClick={handleSendQuestionButtonClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{fetchingState.isLoading && (
|
|
||||||
<p className="w-full py-2 mt-4 flex flex-row justify-center items-center">
|
|
||||||
<Icon.Loader className="w-5 h-auto animate-spin" />
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{historyList.map((history, index) => (
|
|
||||||
<div key={index} className="w-full flex flex-col justify-start items-start mt-4 space-y-2">
|
|
||||||
<div className="w-full flex flex-row justify-start items-start pr-6">
|
|
||||||
<span className="word-break rounded-lg rounded-tl-none px-3 py-2 opacity-80 bg-gray-100 dark:bg-zinc-700">
|
|
||||||
{history.question}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-row justify-end items-start pl-8 space-x-2">
|
|
||||||
<div className="memo-content-wrapper !w-auto flex flex-col justify-start items-start rounded-lg rounded-tr-none px-3 py-2 bg-gray-100 dark:bg-zinc-700">
|
|
||||||
<div className="memo-content-text">{marked(history.answer)}</div>
|
|
||||||
</div>
|
|
||||||
<Icon.Bot className="mt-2 flex-shrink-0 mr-1 w-6 h-auto opacity-80" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{!isEnabled && (
|
|
||||||
<div className="w-full flex flex-col justify-center items-center mt-4 space-y-2">
|
|
||||||
<p>You have not set up your OpenAI API key.</p>
|
|
||||||
<Button onClick={() => handleGotoSystemSetting()}>Go to settings</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -250,16 +250,8 @@ export function deleteIdentityProvider(id: IdentityProviderId) {
|
|||||||
return axios.delete(`/api/idp/${id}`);
|
return axios.delete(`/api/idp/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postChatCompletion(prompt: string) {
|
export function postChatCompletion(messages: any[]) {
|
||||||
return axios.post<ResponseObject<string>>(`/api/openai/chat-completion`, {
|
return axios.post<ResponseObject<string>>(`/api/openai/chat-completion`, messages);
|
||||||
prompt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function postTextCompletion(prompt: string) {
|
|
||||||
return axios.post<ResponseObject<string>>(`/api/openai/text-completion`, {
|
|
||||||
prompt,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkOpenAIEnabled() {
|
export function checkOpenAIEnabled() {
|
||||||
|
26
web/src/store/zustand/message.ts
Normal file
26
web/src/store/zustand/message.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageState {
|
||||||
|
messageList: Message[];
|
||||||
|
getState: () => MessageState;
|
||||||
|
addMessage: (message: Message) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMessageStore = create<MessageState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
messageList: [],
|
||||||
|
getState: () => get(),
|
||||||
|
addMessage: (message: Message) => set((state) => ({ messageList: [...state.messageList, message] })),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "message-storage",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
@ -3058,7 +3058,7 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
use-sync-external-store@^1.0.0:
|
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
@ -3144,3 +3144,10 @@ yocto-queue@^0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zustand@^4.3.6:
|
||||||
|
version "4.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.6.tgz#ce7804eb75361af0461a2d0536b65461ec5de86f"
|
||||||
|
integrity sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "1.2.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user