feat: implement edit chat title modal

This commit is contained in:
steven
2023-03-28 18:31:56 +08:00
parent ac5f4bbcc2
commit ee20272a9e
5 changed files with 121 additions and 10 deletions

View File

@ -58,6 +58,13 @@ const ChatView = () => {
}, [lastMessage?.isGenerated, lastMessage?.content]);
useEffect(() => {
if (
currentChat?.connectionId === connectionStore.currentConnectionCtx?.connection.id &&
currentChat?.databaseName === connectionStore.currentConnectionCtx?.database?.name
) {
return;
}
// Auto select the first chat when the current connection changes.
const chatList = chatStore.chatList.filter(
(chat) =>
@ -65,7 +72,7 @@ const ChatView = () => {
chat.databaseName === connectionStore.currentConnectionCtx?.database?.name
);
chatStore.setCurrentChat(head(chatList));
}, [connectionStore.currentConnectionCtx]);
}, [currentChat, connectionStore.currentConnectionCtx]);
const sendMessageToCurrentChat = async () => {
const currentChat = chatStore.getState().currentChat;

View File

@ -8,11 +8,13 @@ import EngineIcon from "./EngineIcon";
import CreateConnectionModal from "./CreateConnectionModal";
import SettingModal from "./SettingModal";
import ActionConfirmModal, { ActionConfirmModalProps } from "./ActionConfirmModal";
import EditChatTitleModal from "./EditChatTitleModal";
interface State {
showCreateConnectionModal: boolean;
showSettingModal: boolean;
showDeleteConnectionModal: boolean;
showEditChatTitleModal: boolean;
}
const ConnectionSidebar = () => {
@ -23,8 +25,10 @@ const ConnectionSidebar = () => {
showCreateConnectionModal: false,
showSettingModal: false,
showDeleteConnectionModal: false,
showEditChatTitleModal: false,
});
const [deleteConnectionModalContext, setDeleteConnectionModalContext] = useState<ActionConfirmModalProps>();
const [editChatTitleModalContext, setEditChatTitleModalContext] = useState<Chat>();
const connectionList = connectionStore.connectionList;
const currentConnectionCtx = connectionStore.currentConnectionCtx;
const databaseList = connectionStore.databaseList.filter((database) => database.connectionId === currentConnectionCtx?.connection.id);
@ -46,6 +50,13 @@ const ConnectionSidebar = () => {
});
};
const toggleEditChatTitleModal = (show = true) => {
setState({
...state,
showEditChatTitleModal: show,
});
};
const handleConnectionSelect = async (connection: Connection) => {
const databaseList = await connectionStore.getOrFetchDatabaseList(connection);
connectionStore.setCurrentConnectionCtx({
@ -103,6 +114,14 @@ const ConnectionSidebar = () => {
layoutStore.toggleSidebar(false);
};
const handleEditChatTitle = (chat: Chat) => {
setEditChatTitleModalContext(chat);
setState({
...state,
showEditChatTitleModal: true,
});
};
const handleDeleteChat = (chat: Chat) => {
chatStore.clearChat((item) => item.id !== chat.id);
if (chatStore.currentChat?.id === chat.id) {
@ -181,7 +200,7 @@ const ConnectionSidebar = () => {
{chatList.map((chat) => (
<div
key={chat.id}
className={`w-full mt-2 first:mt-4 py-3 px-4 pr-3 rounded-lg flex flex-row justify-start items-center cursor-pointer border border-transparent group hover:bg-gray-50 ${
className={`w-full mt-2 first:mt-4 py-3 pl-4 pr-2 rounded-lg flex flex-row justify-start items-center cursor-pointer border border-transparent group hover:bg-gray-50 ${
chat.id === chatStore.currentChat?.id && "!bg-white border-gray-200 font-medium"
}`}
onClick={() => handleChatSelect(chat)}
@ -192,14 +211,21 @@ const ConnectionSidebar = () => {
<Icon.IoChatbubbleOutline className="w-5 h-auto mr-1.5 opacity-80 shrink-0" />
)}
<span className="truncate grow">{chat.title || "SQL Chat"}</span>
<span
className="ml-0.5 shrink-0 opacity-60 hidden group-hover:block hover:opacity-80"
onClick={(e) => {
e.stopPropagation();
handleDeleteChat(chat);
}}
>
<Icon.IoClose className="w-5 h-auto" />
<span className="ml-0.5 shrink-0 hidden group-hover:flex flex-row justify-end items-center space-x-1">
<Icon.FiEdit3
className="w-4 h-auto opacity-60 hover:opacity-80"
onClick={(e) => {
e.stopPropagation();
handleEditChatTitle(chat);
}}
/>
<Icon.IoClose
className="w-5 h-auto opacity-60 hover:opacity-80"
onClick={(e) => {
e.stopPropagation();
handleDeleteChat(chat);
}}
/>
</span>
</div>
))}
@ -232,6 +258,10 @@ const ConnectionSidebar = () => {
/>,
document.body
)}
{state.showEditChatTitleModal &&
editChatTitleModalContext &&
createPortal(<EditChatTitleModal close={() => toggleEditChatTitleModal(false)} chat={editChatTitleModalContext} />, document.body)}
</>
);
};

View File

@ -0,0 +1,60 @@
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useChatStore } from "@/store";
import { Chat } from "@/types";
import Icon from "./Icon";
interface Props {
chat: Chat;
close: () => void;
}
const EditMessageTitleModal = (props: Props) => {
const { close, chat } = props;
const chatStore = useChatStore();
const [title, setTitle] = useState(chat.title);
const allowSave = title !== "";
const handleSaveEdit = () => {
const formatedTitle = title.trim();
if (formatedTitle === "") {
return;
}
chatStore.updateChat(chat.id, {
title: formatedTitle,
});
toast.success("Chat title updated");
close();
};
return (
<div className="modal modal-middle modal-open">
<div className="modal-box relative">
<h3 className="font-bold text-lg">Edit chat title</h3>
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}>
<Icon.IoMdClose className="w-5 h-auto" />
</button>
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
<input
type="text"
placeholder="Chat title"
className="input input-bordered w-full"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="modal-action">
<button className="btn btn-outline" onClick={close}>
Close
</button>
<button className="btn" disabled={!allowSave} onClick={handleSaveEdit}>
Save
</button>
</div>
</div>
</div>
);
};
export default EditMessageTitleModal;

View File

@ -2,6 +2,7 @@ import * as Ai from "react-icons/ai";
import * as Bi from "react-icons/bi";
import * as Bs from "react-icons/bs";
import * as Di from "react-icons/di";
import * as Fi from "react-icons/fi";
import * as Io from "react-icons/io";
import * as Io5 from "react-icons/io5";
@ -10,6 +11,7 @@ const Icon = {
...Bi,
...Bs,
...Di,
...Fi,
...Io,
...Io5,
};

View File

@ -1,4 +1,5 @@
import dayjs from "dayjs";
import { uniqBy } from "lodash-es";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { Chat, Id } from "@/types";
@ -19,6 +20,7 @@ interface ChatState {
getState: () => ChatState;
createChat: (connectionId?: Id, databaseName?: string) => Chat;
setCurrentChat: (chat: Chat | undefined) => void;
updateChat: (chatId: Id, chat: Partial<Chat>) => void;
clearChat: (filter: (chat: Chat) => boolean) => void;
}
@ -40,6 +42,16 @@ export const useChatStore = create<ChatState>()(
return chat;
},
setCurrentChat: (chat: Chat | undefined) => set(() => ({ currentChat: chat })),
updateChat: (chatId: Id, chat: Partial<Chat>) => {
const rawChat = get().chatList.find((chat) => chat.id === chatId);
if (!rawChat) {
return;
}
Object.assign(rawChat, chat);
set((state) => ({
chatList: uniqBy([...state.chatList], (chat) => chat.id),
}));
},
clearChat: (filter: (chat: Chat) => boolean) => {
set((state) => ({
...state,