add rename and delete improvements
This commit is contained in:
parent
b6d5576227
commit
a0056460ab
|
@ -26,21 +26,21 @@ export const Chat: FC<Props> = ({ model, messages, loading, lightMode, onSend, o
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full w-full flex flex-col dark:bg-[#343541]">
|
||||||
{messages.length === 0 ? (
|
<div className="flex-1 overflow-y-auto">
|
||||||
<>
|
{messages.length === 0 ? (
|
||||||
<div className="flex justify-center pt-8">
|
<>
|
||||||
<ModelSelect
|
<div className="flex justify-center pt-8 overflow-auto">
|
||||||
model={model}
|
<ModelSelect
|
||||||
onSelect={onSelect}
|
model={model}
|
||||||
/>
|
onSelect={onSelect}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 text-4xl text-center text-neutral-300 pt-[280px]">Chatbot UI Pro</div>
|
<div className="flex-1 text-4xl text-center text-neutral-300 pt-[280px]">Chatbot UI Pro</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
<div className="text-center py-3 dark:bg-[#444654] dark:text-neutral-300 text-neutral-500 text-sm border border-b-neutral-300 dark:border-none">Model: {OpenAIModelNames[model]}</div>
|
<div className="text-center py-3 dark:bg-[#444654] dark:text-neutral-300 text-neutral-500 text-sm border border-b-neutral-300 dark:border-none">Model: {OpenAIModelNames[model]}</div>
|
||||||
|
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
|
@ -53,11 +53,11 @@ export const Chat: FC<Props> = ({ model, messages, loading, lightMode, onSend, o
|
||||||
))}
|
))}
|
||||||
{loading && <ChatLoader />}
|
{loading && <ChatLoader />}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<div className="h-[140px] w-[800px] mx-auto">
|
<div className="h-[140px] w-[300px] sm:w-[400px] md:w-[500px] lg:w-[700px] xl:w-[800px] mx-auto">
|
||||||
<ChatInput onSend={onSend} />
|
<ChatInput onSend={onSend} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,11 +6,11 @@ interface Props {}
|
||||||
export const ChatLoader: FC<Props> = () => {
|
export const ChatLoader: FC<Props> = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex justify-center px-[120px] py-[30px] whitespace-pre-wrap dark:bg-[#444654] dark:text-neutral-100 bg-neutral-100 text-neutral-900 dark:border-none"`}
|
className={`flex justify-center py-[30px] whitespace-pre-wrap dark:bg-[#444654] dark:text-neutral-100 bg-neutral-100 text-neutral-900 dark:border-none"`}
|
||||||
style={{ overflowWrap: "anywhere" }}
|
style={{ overflowWrap: "anywhere" }}
|
||||||
>
|
>
|
||||||
<div className="w-[650px] flex">
|
<div className="w-full px-4 sm:px-0 sm:w-2/3 md:w-1/2 flex">
|
||||||
<div className="mr-4 font-bold min-w-[30px]">AI:</div>
|
<div className="mr-4 font-bold min-w-[40px]">AI:</div>
|
||||||
<IconDots className="animate-pulse" />
|
<IconDots className="animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,10 +11,10 @@ interface Props {
|
||||||
export const ChatMessage: FC<Props> = ({ message, lightMode }) => {
|
export const ChatMessage: FC<Props> = ({ message, lightMode }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex justify-center px-[120px] py-[30px] whitespace-pre-wrap] ${message.role === "assistant" ? "dark:bg-[#444654] dark:text-neutral-100 bg-neutral-100 text-neutral-900 border border-neutral-300 dark:border-none" : "dark:bg-[#343541] dark:text-white text-neutral-900"}`}
|
className={`flex justify-center py-[30px] whitespace-pre-wrap ${message.role === "assistant" ? "dark:bg-[#444654] dark:text-neutral-100 bg-neutral-100 text-neutral-900 border border-neutral-300 dark:border-none" : "dark:bg-[#343541] dark:text-white text-neutral-900"}`}
|
||||||
style={{ overflowWrap: "anywhere" }}
|
style={{ overflowWrap: "anywhere" }}
|
||||||
>
|
>
|
||||||
<div className="w-[650px] flex align-middle">
|
<div className="w-full px-4 sm:px-0 sm:w-2/3 md:w-1/2 flex">
|
||||||
<div className="mr-4 font-bold min-w-[40px]">{message.role === "assistant" ? "AI:" : "You:"}</div>
|
<div className="mr-4 font-bold min-w-[40px]">{message.role === "assistant" ? "AI:" : "You:"}</div>
|
||||||
|
|
||||||
<div className="prose dark:prose-invert">
|
<div className="prose dark:prose-invert">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Conversation } from "@/types";
|
import { Conversation } from "@/types";
|
||||||
import { IconMessage, IconTrash } from "@tabler/icons-react";
|
import { IconCheck, IconMessage, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
import { FC } from "react";
|
import { FC, KeyboardEvent, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -8,15 +8,41 @@ interface Props {
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
|
onRenameConversation: (conversation: Conversation, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Conversations: FC<Props> = ({ loading, conversations, selectedConversation, onSelectConversation, onDeleteConversation }) => {
|
export const Conversations: FC<Props> = ({ loading, conversations, selectedConversation, onSelectConversation, onDeleteConversation, onRenameConversation }) => {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState("");
|
||||||
|
|
||||||
|
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRename(selectedConversation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = (conversation: Conversation) => {
|
||||||
|
onRenameConversation(conversation, renameValue);
|
||||||
|
setRenameValue("");
|
||||||
|
setIsRenaming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
setIsDeleting(false);
|
||||||
|
} else if (isDeleting) {
|
||||||
|
setIsRenaming(false);
|
||||||
|
}
|
||||||
|
}, [isRenaming, isDeleting]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2 w-full px-2">
|
||||||
{conversations.map((conversation, index) => (
|
{conversations.map((conversation, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
className={`flex items-center justify-start w-[240px] h-[40px] px-2 text-sm rounded-lg hover:bg-neutral-700 cursor-pointer ${loading ? "disabled:cursor-not-allowed" : ""} ${selectedConversation.id === conversation.id ? "bg-slate-600" : ""}`}
|
className={`flex items-center justify-start h-[40px] px-2 text-sm rounded-lg hover:bg-neutral-700 cursor-pointer ${loading ? "disabled:cursor-not-allowed" : ""} ${selectedConversation.id === conversation.id ? "bg-slate-600" : ""}`}
|
||||||
onClick={() => onSelectConversation(conversation)}
|
onClick={() => onSelectConversation(conversation)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
@ -24,16 +50,73 @@ export const Conversations: FC<Props> = ({ loading, conversations, selectedConve
|
||||||
className="mr-2 min-w-[20px]"
|
className="mr-2 min-w-[20px]"
|
||||||
size={18}
|
size={18}
|
||||||
/>
|
/>
|
||||||
<div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1">{conversation.messages[0] ? conversation.messages[0].content : "Empty conversation"}</div>
|
|
||||||
|
|
||||||
<IconTrash
|
{isRenaming && selectedConversation.id === conversation.id ? (
|
||||||
className="ml-auto min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
<input
|
||||||
size={18}
|
className="flex-1 bg-transparent border-b border-neutral-400 focus:border-neutral-100 text-left overflow-hidden overflow-ellipsis pr-1 outline-none text-white"
|
||||||
onClick={(e) => {
|
type="text"
|
||||||
e.stopPropagation();
|
value={renameValue}
|
||||||
onDeleteConversation(conversation);
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
}}
|
onKeyDown={handleEnterDown}
|
||||||
/>
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1 flex-1 text-left">{conversation.name}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isDeleting || isRenaming) && selectedConversation.id === conversation.id && (
|
||||||
|
<div className="flex w-[40px]">
|
||||||
|
<IconCheck
|
||||||
|
className="ml-auto min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isDeleting) {
|
||||||
|
onDeleteConversation(conversation);
|
||||||
|
} else if (isRenaming) {
|
||||||
|
handleRename(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsRenaming(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconX
|
||||||
|
className="ml-auto min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsRenaming(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedConversation.id === conversation.id && !isDeleting && !isRenaming && (
|
||||||
|
<div className="flex w-[40px]">
|
||||||
|
<IconPencil
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsRenaming(true);
|
||||||
|
setRenameValue(selectedConversation.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconTrash
|
||||||
|
className=" min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,14 +14,15 @@ interface Props {
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
onToggleSidebar: () => void;
|
onToggleSidebar: () => void;
|
||||||
|
onRenameConversation: (conversation: Conversation, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar }) => {
|
export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onRenameConversation }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-[#202123] min-w-[260px]">
|
<div className="flex flex-col bg-[#202123] min-w-[260px]">
|
||||||
<div className="flex items-center h-[60px] pl-4">
|
<div className="flex items-center h-[60px] pl-2">
|
||||||
<button
|
<button
|
||||||
className="flex items-center w-[190px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700"
|
className="flex items-center w-[220px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700"
|
||||||
onClick={onNewConversation}
|
onClick={onNewConversation}
|
||||||
>
|
>
|
||||||
<IconPlus
|
<IconPlus
|
||||||
|
@ -32,18 +33,19 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<IconArrowBarLeft
|
<IconArrowBarLeft
|
||||||
className="ml-auto mr-4 text-neutral-300 cursor-pointer hover:text-neutral-400"
|
className="ml-1 mr-2 text-neutral-300 cursor-pointer hover:text-neutral-400"
|
||||||
onClick={onToggleSidebar}
|
onClick={onToggleSidebar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 mx-auto pb-2 overflow-auto">
|
<div className="flex flex-1 justify-center pb-2 overflow-y-auto">
|
||||||
<Conversations
|
<Conversations
|
||||||
loading={loading}
|
loading={loading}
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
selectedConversation={selectedConversation}
|
selectedConversation={selectedConversation}
|
||||||
onSelectConversation={onSelectConversation}
|
onSelectConversation={onSelectConversation}
|
||||||
onDeleteConversation={onDeleteConversation}
|
onDeleteConversation={onDeleteConversation}
|
||||||
|
onRenameConversation={onRenameConversation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ export default function Home() {
|
||||||
|
|
||||||
updatedConversation = {
|
updatedConversation = {
|
||||||
...updatedConversation,
|
...updatedConversation,
|
||||||
|
name: message.content,
|
||||||
messages: updatedMessages
|
messages: updatedMessages
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -120,12 +121,34 @@ export default function Home() {
|
||||||
localStorage.setItem("theme", mode);
|
localStorage.setItem("theme", mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRenameConversation = (conversation: Conversation, name: string) => {
|
||||||
|
const updatedConversations = conversations.map((c) => {
|
||||||
|
if (c.id === conversation.id) {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
setConversations(updatedConversations);
|
||||||
|
localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations));
|
||||||
|
|
||||||
|
setSelectedConversation({
|
||||||
|
...conversation,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
localStorage.setItem("selectedConversation", JSON.stringify(selectedConversation));
|
||||||
|
};
|
||||||
|
|
||||||
const handleNewConversation = () => {
|
const handleNewConversation = () => {
|
||||||
const lastConversation = conversations[conversations.length - 1];
|
const lastConversation = conversations[conversations.length - 1];
|
||||||
|
|
||||||
const newConversation: Conversation = {
|
const newConversation: Conversation = {
|
||||||
id: lastConversation ? lastConversation.id + 1 : 1,
|
id: lastConversation ? lastConversation.id + 1 : 1,
|
||||||
name: "",
|
name: "New conversation",
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -218,6 +241,7 @@ export default function Home() {
|
||||||
onSelectConversation={handleSelectConversation}
|
onSelectConversation={handleSelectConversation}
|
||||||
onDeleteConversation={handleDeleteConversation}
|
onDeleteConversation={handleDeleteConversation}
|
||||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||||
|
onRenameConversation={handleRenameConversation}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<IconArrowBarRight
|
<IconArrowBarRight
|
||||||
|
@ -226,16 +250,14 @@ export default function Home() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col w-full h-full dark:bg-[#343541]">
|
<Chat
|
||||||
<Chat
|
model={model}
|
||||||
model={model}
|
messages={selectedConversation.messages}
|
||||||
messages={selectedConversation.messages}
|
loading={loading}
|
||||||
loading={loading}
|
lightMode={lightMode}
|
||||||
lightMode={lightMode}
|
onSend={handleSend}
|
||||||
onSend={handleSend}
|
onSelect={setModel}
|
||||||
onSelect={setModel}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Reference in New Issue