mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 21:13:59 +01:00
parent
404dae8dff
commit
fe8d6ccced
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "Indlæg på ShareGPT",
|
"title": "Indlæg på ShareGPT",
|
||||||
"warning": "Vær opmærksom på, at ved at poste din samtale på ShareGPT, vil den blive offentligt tilgængelig og synlig for alle. Når den er postet, kan samtalen ikke skjules eller slettes og kan blive arkiveret eller delt af andre. Vi råder dig til at overveje nøje og undgå at dele følsomme eller private oplysninger på denne platform."
|
"warning": "Vær opmærksom på, at ved at poste din samtale på ShareGPT, vil den blive offentligt tilgængelig og synlig for alle. Når den er postet, kan samtalen ikke skjules eller slettes og kan blive arkiveret eller delt af andre. Vi råder dig til at overveje nøje og undgå at dele følsomme eller private oplysninger på denne platform."
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "Post on ShareGPT",
|
"title": "Post on ShareGPT",
|
||||||
"warning": "Please be aware that by posting your conversation on ShareGPT, it will become publicly accessible and viewable to anyone. Once posted, the conversation cannot be hidden or deleted, and may be archived or shared by others. We advise you to consider carefully and avoid sharing sensitive or private information on this platform."
|
"warning": "Please be aware that by posting your conversation on ShareGPT, it will become publicly accessible and viewable to anyone. Once posted, the conversation cannot be hidden or deleted, and may be archived or shared by others. We advise you to consider carefully and avoid sharing sensitive or private information on this platform."
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "Publicar en ShareGPT",
|
"title": "Publicar en ShareGPT",
|
||||||
"warning": "Por favor, tenga en cuenta que al publicar su conversación en ShareGPT, esta será accesible y visible para cualquiera. Una vez publicada, la conversación no se podrá ocultar ni eliminar, y puede ser archivada o compartida por otros. Le aconsejamos que lo considere detenidamente y evite compartir información sensible o privada en esta plataforma."
|
"warning": "Por favor, tenga en cuenta que al publicar su conversación en ShareGPT, esta será accesible y visible para cualquiera. Una vez publicada, la conversación no se podrá ocultar ni eliminar, y puede ser archivada o compartida por otros. Le aconsejamos que lo considere detenidamente y evite compartir información sensible o privada en esta plataforma."
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "ShareGPTに投稿",
|
"title": "ShareGPTに投稿",
|
||||||
"warning": "ShareGPTに会話を投稿すると、誰でもアクセスして閲覧できるようになることに注意してください。一度投稿すると、会話は非表示にできず、削除もできません。また、他の人がアーカイブや共有する可能性があります。このプラットフォームで機密性のある情報や個人情報を共有しないように注意してください。"
|
"warning": "ShareGPTに会話を投稿すると、誰でもアクセスして閲覧できるようになることに注意してください。一度投稿すると、会話は非表示にできず、削除もできません。また、他の人がアーカイブや共有する可能性があります。このプラットフォームで機密性のある情報や個人情報を共有しないように注意してください。"
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "Siarkan di ShareGPT",
|
"title": "Siarkan di ShareGPT",
|
||||||
"warning": "Sila ambil perhatian bahawa dengan menyiarkan perbualan anda di ShareGPT, ia akan menjadi boleh diakses dan dilihat oleh sesiapa sahaja. Setelah disiarkan, perbualan tidak boleh disembunyikan atau dipadam, dan mungkin diarkibkan atau dikongsi oleh orang lain. Kami menasihatkan anda untuk mempertimbangkan dengan teliti dan mengelakkan berkongsi maklumat sensitif atau peribadi di platform ini."
|
"warning": "Sila ambil perhatian bahawa dengan menyiarkan perbualan anda di ShareGPT, ia akan menjadi boleh diakses dan dilihat oleh sesiapa sahaja. Setelah disiarkan, perbualan tidak boleh disembunyikan atau dipadam, dan mungkin diarkibkan atau dikongsi oleh orang lain. Kami menasihatkan anda untuk mempertimbangkan dengan teliti dan mengelakkan berkongsi maklumat sensitif atau peribadi di platform ini."
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "Innlegg på ShareGPT",
|
"title": "Innlegg på ShareGPT",
|
||||||
"warning": "Vær oppmerksom på at ved å poste samtalen din på ShareGPT, vil den bli offentlig tilgjengelig og synlig for alle. Når den er postet, kan samtalen ikke skjules eller slettes, og den kan bli arkivert eller delt av andre. Vi anbefaler deg å tenke nøye gjennom og unngå å dele sensitiv eller privat informasjon på denne plattformen."
|
"warning": "Vær oppmerksom på at ved å poste samtalen din på ShareGPT, vil den bli offentlig tilgjengelig og synlig for alle. Når den er postet, kan samtalen ikke skjules eller slettes, og den kan bli arkivert eller delt av andre. Vi anbefaler deg å tenke nøye gjennom og unngå å dele sensitiv eller privat informasjon på denne plattformen."
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "Inlägg på ShareGPT",
|
"title": "Inlägg på ShareGPT",
|
||||||
"warning": "Var medveten om att genom att posta din konversation på ShareGPT kommer den att bli offentligt tillgänglig och synlig för alla. När den väl är postad kan konversationen varken döljas eller raderas och kan arkiveras eller delas av andra. Vi rekommenderar dig att tänka noggrant igenom och undvika att dela känslig eller privat information på denna plattform."
|
"warning": "Var medveten om att genom att posta din konversation på ShareGPT kommer den att bli offentligt tillgänglig och synlig för alla. När den väl är postad kan konversationen varken döljas eller raderas och kan arkiveras eller delas av andra. Vi rekommenderar dig att tänka noggrant igenom och undvika att dela känslig eller privat information på denna plattform."
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "发布至 ShareGPT",
|
"title": "发布至 ShareGPT",
|
||||||
"warning": "请注意,把您的对话发布到 ShareGPT 后,任何人都可以公开访问和查看。发布后,对话不能被隐藏或删除,且可能被其他人存档或分享。建议您慎重考虑,在这个平台上避免分享敏感或私密信息。"
|
"warning": "请注意,把您的对话发布到 ShareGPT 后,任何人都可以公开访问和查看。发布后,对话不能被隐藏或删除,且可能被其他人存档或分享。建议您慎重考虑,在这个平台上避免分享敏感或私密信息。"
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "po 上 ShareGPT",
|
"title": "po 上 ShareGPT",
|
||||||
"warning": "請注意,你將呢個傾偈 po 上 ShareGPT 之後,佢會係公開嘅,所有人都可以見到你寫嘅嘢。一旦 po 咗,呢個傾偈將冇得被隱藏或刪除,亦都可能畀人存檔同分享。我哋建議你諗清楚,唔好喺嗰度分享敏感或私人資料。"
|
"warning": "請注意,你將呢個傾偈 po 上 ShareGPT 之後,佢會係公開嘅,所有人都可以見到你寫嘅嘢。一旦 po 咗,呢個傾偈將冇得被隱藏或刪除,亦都可能畀人存檔同分享。我哋建議你諗清楚,唔好喺嗰度分享敏感或私人資料。"
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,6 @@
|
||||||
"postOnShareGPT": {
|
"postOnShareGPT": {
|
||||||
"title": "發佈至 ShareGPT",
|
"title": "發佈至 ShareGPT",
|
||||||
"warning": "請注意,將您的對話發佈至 ShareGPT 後,任何人都可以公開訪問和查看。一旦發佈,對話將無法隱藏或刪除,並且可能被他人存檔或分享。我們建議您慎重考慮,並避免在此平台上分享敏感或私人信息。"
|
"warning": "請注意,將您的對話發佈至 ShareGPT 後,任何人都可以公開訪問和查看。一旦發佈,對話將無法隱藏或刪除,並且可能被他人存檔或分享。我們建議您慎重考慮,並避免在此平台上分享敏感或私人信息。"
|
||||||
}
|
},
|
||||||
|
"newFolder": "New Folder"
|
||||||
}
|
}
|
||||||
|
|
17
src/assets/icons/FolderIcon.tsx
Normal file
17
src/assets/icons/FolderIcon.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const FolderIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox='0 0 1024 1024'
|
||||||
|
fill='currentColor'
|
||||||
|
height='1em'
|
||||||
|
width='1em'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d='M880 298.4H521L403.7 186.2a8.15 8.15 0 00-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FolderIcon;
|
188
src/components/Menu/ChatFolder.tsx
Normal file
188
src/components/Menu/ChatFolder.tsx
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import useStore from '@store/store';
|
||||||
|
|
||||||
|
import DownChevronArrow from '@icon/DownChevronArrow';
|
||||||
|
import FolderIcon from '@icon/FolderIcon';
|
||||||
|
import { ChatHistoryInterface, ChatInterface } from '@type/chat';
|
||||||
|
|
||||||
|
import ChatHistory from './ChatHistory';
|
||||||
|
import EditIcon from '@icon/EditIcon';
|
||||||
|
import DeleteIcon from '@icon/DeleteIcon';
|
||||||
|
import CrossIcon from '@icon/CrossIcon';
|
||||||
|
import TickIcon from '@icon/TickIcon';
|
||||||
|
|
||||||
|
const ChatFolder = ({
|
||||||
|
folderName,
|
||||||
|
folderChats,
|
||||||
|
folderIndex,
|
||||||
|
}: {
|
||||||
|
folderName: string;
|
||||||
|
folderChats: ChatHistoryInterface[];
|
||||||
|
folderIndex: number;
|
||||||
|
}) => {
|
||||||
|
const setChats = useStore((state) => state.setChats);
|
||||||
|
const setFoldersName = useStore((state) => state.setFoldersName);
|
||||||
|
const setFoldersExpanded = useStore((state) => state.setFoldersExpanded);
|
||||||
|
const foldersExpanded = useStore((state) => state.foldersExpanded);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [_folderName, _setFolderName] = useState<string>(folderName);
|
||||||
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
|
const [isDelete, setIsDelete] = useState<boolean>(false);
|
||||||
|
// const [isExpanded, setIsExpanded] = useState<boolean>(
|
||||||
|
// useStore.getState().foldersExpanded[folderIndex]
|
||||||
|
// );
|
||||||
|
const [isHover, setIsHover] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleTick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const updatedChats: ChatInterface[] = JSON.parse(
|
||||||
|
JSON.stringify(useStore.getState().chats)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
updatedChats.forEach((chat) => {
|
||||||
|
if (chat.folder === folderName) chat.folder = _folderName;
|
||||||
|
});
|
||||||
|
setChats(updatedChats);
|
||||||
|
|
||||||
|
const updatedFolderNames = [...useStore.getState().foldersName];
|
||||||
|
const pos = updatedFolderNames.indexOf(folderName);
|
||||||
|
if (pos !== -1) updatedFolderNames[pos] = _folderName;
|
||||||
|
setFoldersName(updatedFolderNames);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
} else if (isDelete) {
|
||||||
|
updatedChats.forEach((chat) => {
|
||||||
|
if (chat.folder === folderName) delete chat.folder;
|
||||||
|
});
|
||||||
|
setChats(updatedChats);
|
||||||
|
|
||||||
|
setFoldersName(
|
||||||
|
useStore.getState().foldersName.filter((name) => name !== folderName)
|
||||||
|
);
|
||||||
|
setIsDelete(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCross = () => {
|
||||||
|
setIsDelete(false);
|
||||||
|
setIsEdit(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsHover(false);
|
||||||
|
|
||||||
|
const updatedFoldersExpanded = [...foldersExpanded];
|
||||||
|
updatedFoldersExpanded[folderIndex] = true;
|
||||||
|
setFoldersExpanded(updatedFoldersExpanded);
|
||||||
|
|
||||||
|
const chatIndex = Number(e.dataTransfer.getData('chatIndex'));
|
||||||
|
const updatedChats: ChatInterface[] = JSON.parse(
|
||||||
|
JSON.stringify(useStore.getState().chats)
|
||||||
|
);
|
||||||
|
updatedChats[chatIndex].folder = folderName;
|
||||||
|
setChats(updatedChats);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsHover(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
setIsHover(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExpanded = () => {
|
||||||
|
const updatedFoldersExpanded = [...foldersExpanded];
|
||||||
|
updatedFoldersExpanded[folderIndex] = !updatedFoldersExpanded[folderIndex];
|
||||||
|
setFoldersExpanded(updatedFoldersExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full transition-colors ${isHover ? 'bg-gray-800/40' : ''}`}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='flex py-3 px-3 items-center gap-3 relative rounded-md hover:bg-[#2A2B32] break-all cursor-pointer'
|
||||||
|
onClick={toggleExpanded}
|
||||||
|
>
|
||||||
|
<FolderIcon className='h-4 w-4' />
|
||||||
|
<div className='flex-1 text-ellipsis max-h-5 overflow-hidden break-all relative'>
|
||||||
|
{isEdit ? (
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='focus:outline-blue-600 text-sm border-none bg-transparent p-0 m-0 w-full'
|
||||||
|
value={_folderName}
|
||||||
|
onChange={(e) => {
|
||||||
|
_setFolderName(e.target.value);
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
_folderName
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='absolute flex right-1 z-10 text-gray-300 visible'
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{isDelete || isEdit ? (
|
||||||
|
<>
|
||||||
|
<button className='p-1 hover:text-white' onClick={handleTick}>
|
||||||
|
<TickIcon />
|
||||||
|
</button>
|
||||||
|
<button className='p-1 hover:text-white' onClick={handleCross}>
|
||||||
|
<CrossIcon />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className='p-1 hover:text-white'
|
||||||
|
onClick={() => setIsEdit(true)}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='p-1 hover:text-white'
|
||||||
|
onClick={() => setIsDelete(true)}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</button>
|
||||||
|
<button className='p-1 hover:text-white' onClick={toggleExpanded}>
|
||||||
|
<DownChevronArrow
|
||||||
|
className={`${
|
||||||
|
foldersExpanded[folderIndex] ? 'rotate-180' : ''
|
||||||
|
} transition-transform`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='ml-3 pl-1 border-l-2 border-gray-700 flex flex-col gap-1'>
|
||||||
|
{foldersExpanded[folderIndex] &&
|
||||||
|
folderChats.map((chat) => (
|
||||||
|
<ChatHistory
|
||||||
|
title={chat.title}
|
||||||
|
chatIndex={chat.index}
|
||||||
|
key={`${chat.title}-${chat.index}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatFolder;
|
147
src/components/Menu/ChatHistory.tsx
Normal file
147
src/components/Menu/ChatHistory.tsx
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
|
||||||
|
|
||||||
|
import ChatIcon from '@icon/ChatIcon';
|
||||||
|
import CrossIcon from '@icon/CrossIcon';
|
||||||
|
import DeleteIcon from '@icon/DeleteIcon';
|
||||||
|
import EditIcon from '@icon/EditIcon';
|
||||||
|
import TickIcon from '@icon/TickIcon';
|
||||||
|
import useStore from '@store/store';
|
||||||
|
|
||||||
|
const ChatHistoryClass = {
|
||||||
|
normal:
|
||||||
|
'flex py-3 px-3 items-center gap-3 relative rounded-md bg-gray-900 hover:bg-[#2A2B32] break-all hover:pr-4 group transition-opacity',
|
||||||
|
active:
|
||||||
|
'flex py-3 px-3 items-center gap-3 relative rounded-md break-all pr-14 bg-gray-800 hover:bg-gray-800 group transition-opacity',
|
||||||
|
normalGradient:
|
||||||
|
'absolute inset-y-0 right-0 w-8 z-10 bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]',
|
||||||
|
activeGradient:
|
||||||
|
'absolute inset-y-0 right-0 w-8 z-10 bg-gradient-to-l from-gray-800',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChatHistory = React.memo(
|
||||||
|
({ title, chatIndex }: { title: string; chatIndex: number }) => {
|
||||||
|
const initialiseNewChat = useInitialiseNewChat();
|
||||||
|
const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
|
||||||
|
const setChats = useStore((state) => state.setChats);
|
||||||
|
const active = useStore((state) => state.currentChatIndex === chatIndex);
|
||||||
|
const generating = useStore((state) => state.generating);
|
||||||
|
|
||||||
|
const [isDelete, setIsDelete] = useState<boolean>(false);
|
||||||
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
|
const [_title, _setTitle] = useState<string>(title);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleTick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const updatedChats = JSON.parse(
|
||||||
|
JSON.stringify(useStore.getState().chats)
|
||||||
|
);
|
||||||
|
if (isEdit) {
|
||||||
|
updatedChats[chatIndex].title = _title;
|
||||||
|
setChats(updatedChats);
|
||||||
|
setIsEdit(false);
|
||||||
|
} else if (isDelete) {
|
||||||
|
updatedChats.splice(chatIndex, 1);
|
||||||
|
if (updatedChats.length > 0) {
|
||||||
|
setCurrentChatIndex(0);
|
||||||
|
setChats(updatedChats);
|
||||||
|
} else {
|
||||||
|
initialiseNewChat();
|
||||||
|
}
|
||||||
|
setIsDelete(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCross = () => {
|
||||||
|
setIsDelete(false);
|
||||||
|
setIsEdit(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.DragEvent<HTMLAnchorElement>) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.setData('chatIndex', String(chatIndex));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef && inputRef.current) inputRef.current.focus();
|
||||||
|
}, [isEdit]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={`${
|
||||||
|
active ? ChatHistoryClass.active : ChatHistoryClass.normal
|
||||||
|
} ${
|
||||||
|
generating
|
||||||
|
? 'cursor-not-allowed opacity-40'
|
||||||
|
: 'cursor-pointer opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!generating) setCurrentChatIndex(chatIndex);
|
||||||
|
}}
|
||||||
|
draggable
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
>
|
||||||
|
<ChatIcon />
|
||||||
|
<div className='flex-1 text-ellipsis max-h-5 overflow-hidden break-all relative'>
|
||||||
|
{isEdit ? (
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='focus:outline-blue-600 text-sm border-none bg-transparent p-0 m-0 w-full'
|
||||||
|
value={_title}
|
||||||
|
onChange={(e) => {
|
||||||
|
_setTitle(e.target.value);
|
||||||
|
}}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
_title
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEdit || (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
active
|
||||||
|
? ChatHistoryClass.activeGradient
|
||||||
|
: ChatHistoryClass.normalGradient
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{active && (
|
||||||
|
<div className='absolute flex right-1 z-10 text-gray-300 visible'>
|
||||||
|
{isDelete || isEdit ? (
|
||||||
|
<>
|
||||||
|
<button className='p-1 hover:text-white' onClick={handleTick}>
|
||||||
|
<TickIcon />
|
||||||
|
</button>
|
||||||
|
<button className='p-1 hover:text-white' onClick={handleCross}>
|
||||||
|
<CrossIcon />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className='p-1 hover:text-white'
|
||||||
|
onClick={() => setIsEdit(true)}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='p-1 hover:text-white'
|
||||||
|
onClick={() => setIsDelete(true)}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ChatHistory;
|
|
@ -2,21 +2,71 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||||
import useStore from '@store/store';
|
import useStore from '@store/store';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
import ChatIcon from '@icon/ChatIcon';
|
import NewFolder from './NewFolder';
|
||||||
import EditIcon from '@icon/EditIcon';
|
import ChatFolder from './ChatFolder';
|
||||||
import DeleteIcon from '@icon/DeleteIcon';
|
import ChatHistory from './ChatHistory';
|
||||||
import TickIcon from '@icon/TickIcon';
|
|
||||||
import CrossIcon from '@icon/CrossIcon';
|
|
||||||
|
|
||||||
import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
|
import {
|
||||||
|
ChatHistoryInterface,
|
||||||
|
ChatHistoryFolderInterface,
|
||||||
|
ChatInterface,
|
||||||
|
} from '@type/chat';
|
||||||
|
|
||||||
const ChatHistoryList = () => {
|
const ChatHistoryList = () => {
|
||||||
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
||||||
|
const setChats = useStore((state) => state.setChats);
|
||||||
const chatTitles = useStore(
|
const chatTitles = useStore(
|
||||||
(state) => state.chats?.map((chat) => chat.title),
|
(state) => state.chats?.map((chat) => chat.title),
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isHover, setIsHover] = useState<boolean>(false);
|
||||||
|
const [folders, setFolders] = useState<ChatHistoryFolderInterface>({});
|
||||||
|
const [noFolders, setNoFolders] = useState<ChatHistoryInterface[]>([]);
|
||||||
|
const chatsRef = useRef<ChatInterface[]>(useStore.getState().chats || []);
|
||||||
|
const foldersNameRef = useRef<string[]>(useStore.getState().foldersName);
|
||||||
|
|
||||||
|
const updateFolders = () => {
|
||||||
|
const _folders: ChatHistoryFolderInterface = {};
|
||||||
|
const _noFolders: ChatHistoryInterface[] = [];
|
||||||
|
const chats = useStore.getState().chats;
|
||||||
|
const foldersName = useStore.getState().foldersName;
|
||||||
|
|
||||||
|
foldersName.forEach((f) => (_folders[f] = []));
|
||||||
|
|
||||||
|
if (chats) {
|
||||||
|
chats.forEach((chat, index) => {
|
||||||
|
if (!chat.folder) {
|
||||||
|
_noFolders.push({ title: chat.title, index: index });
|
||||||
|
} else {
|
||||||
|
if (!_folders[chat.folder]) _folders[chat.folder] = [];
|
||||||
|
_folders[chat.folder].push({ title: chat.title, index: index });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFolders(_folders);
|
||||||
|
setNoFolders(_noFolders);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFolders();
|
||||||
|
|
||||||
|
useStore.subscribe((state) => {
|
||||||
|
if (
|
||||||
|
!state.generating &&
|
||||||
|
state.chats &&
|
||||||
|
state.chats !== chatsRef.current
|
||||||
|
) {
|
||||||
|
updateFolders();
|
||||||
|
chatsRef.current = state.chats;
|
||||||
|
} else if (state.foldersName !== foldersNameRef.current) {
|
||||||
|
updateFolders();
|
||||||
|
foldersNameRef.current = state.foldersName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
chatTitles &&
|
chatTitles &&
|
||||||
|
@ -24,22 +74,80 @@ const ChatHistoryList = () => {
|
||||||
currentChatIndex < chatTitles.length
|
currentChatIndex < chatTitles.length
|
||||||
) {
|
) {
|
||||||
document.title = chatTitles[currentChatIndex];
|
document.title = chatTitles[currentChatIndex];
|
||||||
|
|
||||||
|
const chats = useStore.getState().chats;
|
||||||
|
if (chats) {
|
||||||
|
const folderIndex = useStore
|
||||||
|
.getState()
|
||||||
|
.foldersName.findIndex((f) => f === chats[currentChatIndex].folder);
|
||||||
|
|
||||||
|
if (folderIndex) {
|
||||||
|
const updatedFolderExpanded = [
|
||||||
|
...useStore.getState().foldersExpanded,
|
||||||
|
];
|
||||||
|
updatedFolderExpanded[folderIndex] = true;
|
||||||
|
useStore.getState().setFoldersExpanded(updatedFolderExpanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [currentChatIndex, chatTitles]);
|
}, [currentChatIndex, chatTitles]);
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsHover(false);
|
||||||
|
|
||||||
|
const chatIndex = Number(e.dataTransfer.getData('chatIndex'));
|
||||||
|
const updatedChats: ChatInterface[] = JSON.parse(
|
||||||
|
JSON.stringify(useStore.getState().chats)
|
||||||
|
);
|
||||||
|
delete updatedChats[chatIndex].folder;
|
||||||
|
setChats(updatedChats);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsHover(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
setIsHover(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
setIsHover(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex-col flex-1 overflow-y-auto border-b border-white/20'>
|
<div
|
||||||
|
className={`flex-col flex-1 overflow-y-auto border-b border-white/20 ${
|
||||||
|
isHover ? 'bg-gray-800/40' : ''
|
||||||
|
}`}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<NewFolder />
|
||||||
<div className='flex flex-col gap-2 text-gray-100 text-sm'>
|
<div className='flex flex-col gap-2 text-gray-100 text-sm'>
|
||||||
{chatTitles &&
|
{Object.keys(folders).map((folderName, folderIndex) => (
|
||||||
chatTitles.map((title, index) => (
|
<ChatFolder
|
||||||
|
folderName={folderName}
|
||||||
|
folderChats={folders[folderName]}
|
||||||
|
folderIndex={folderIndex}
|
||||||
|
key={folderName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{noFolders.map(({ title, index }) => (
|
||||||
<ChatHistory
|
<ChatHistory
|
||||||
title={title}
|
title={title}
|
||||||
key={`${title}-${index}`}
|
key={`${title}-${index}`}
|
||||||
chatIndex={index}
|
chatIndex={index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* <ShowMoreButton /> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className='w-full h-10' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -52,131 +160,4 @@ const ShowMoreButton = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatHistoryClass = {
|
|
||||||
normal:
|
|
||||||
'flex py-3 px-3 items-center gap-3 relative rounded-md hover:bg-[#2A2B32] break-all hover:pr-4 group transition-opacity',
|
|
||||||
active:
|
|
||||||
'flex py-3 px-3 items-center gap-3 relative rounded-md break-all pr-14 bg-gray-800 hover:bg-gray-800 group transition-opacity',
|
|
||||||
normalGradient:
|
|
||||||
'absolute inset-y-0 right-0 w-8 z-10 bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]',
|
|
||||||
activeGradient:
|
|
||||||
'absolute inset-y-0 right-0 w-8 z-10 bg-gradient-to-l from-gray-800',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChatHistory = React.memo(
|
|
||||||
({ title, chatIndex }: { title: string; chatIndex: number }) => {
|
|
||||||
const initialiseNewChat = useInitialiseNewChat();
|
|
||||||
const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
|
|
||||||
const setChats = useStore((state) => state.setChats);
|
|
||||||
const active = useStore((state) => state.currentChatIndex === chatIndex);
|
|
||||||
const generating = useStore((state) => state.generating);
|
|
||||||
|
|
||||||
const [isDelete, setIsDelete] = useState<boolean>(false);
|
|
||||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
|
||||||
const [_title, _setTitle] = useState<string>(title);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleTick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const updatedChats = JSON.parse(
|
|
||||||
JSON.stringify(useStore.getState().chats)
|
|
||||||
);
|
|
||||||
if (isEdit) {
|
|
||||||
updatedChats[chatIndex].title = _title;
|
|
||||||
setChats(updatedChats);
|
|
||||||
setIsEdit(false);
|
|
||||||
} else if (isDelete) {
|
|
||||||
updatedChats.splice(chatIndex, 1);
|
|
||||||
if (updatedChats.length > 0) {
|
|
||||||
setCurrentChatIndex(0);
|
|
||||||
setChats(updatedChats);
|
|
||||||
} else {
|
|
||||||
initialiseNewChat();
|
|
||||||
}
|
|
||||||
setIsDelete(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCross = () => {
|
|
||||||
setIsDelete(false);
|
|
||||||
setIsEdit(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef && inputRef.current) inputRef.current.focus();
|
|
||||||
}, [isEdit]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className={`${
|
|
||||||
active ? ChatHistoryClass.active : ChatHistoryClass.normal
|
|
||||||
} ${
|
|
||||||
generating
|
|
||||||
? 'cursor-not-allowed opacity-40'
|
|
||||||
: 'cursor-pointer opacity-100'
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (!generating) setCurrentChatIndex(chatIndex);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChatIcon />
|
|
||||||
<div className='flex-1 text-ellipsis max-h-5 overflow-hidden break-all relative'>
|
|
||||||
{isEdit ? (
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
className='focus:outline-blue-600 text-sm border-none bg-transparent p-0 m-0 w-full'
|
|
||||||
value={_title}
|
|
||||||
onChange={(e) => {
|
|
||||||
_setTitle(e.target.value);
|
|
||||||
}}
|
|
||||||
ref={inputRef}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
_title
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isEdit || (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? ChatHistoryClass.activeGradient
|
|
||||||
: ChatHistoryClass.normalGradient
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{active && (
|
|
||||||
<div className='absolute flex right-1 z-10 text-gray-300 visible'>
|
|
||||||
{isDelete || isEdit ? (
|
|
||||||
<>
|
|
||||||
<button className='p-1 hover:text-white' onClick={handleTick}>
|
|
||||||
<TickIcon />
|
|
||||||
</button>
|
|
||||||
<button className='p-1 hover:text-white' onClick={handleCross}>
|
|
||||||
<CrossIcon />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className='p-1 hover:text-white'
|
|
||||||
onClick={() => setIsEdit(true)}
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='p-1 hover:text-white'
|
|
||||||
onClick={() => setIsDelete(true)}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ChatHistoryList;
|
export default ChatHistoryList;
|
||||||
|
|
47
src/components/Menu/NewFolder.tsx
Normal file
47
src/components/Menu/NewFolder.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import useStore from '@store/store';
|
||||||
|
|
||||||
|
import PlusIcon from '@icon/PlusIcon';
|
||||||
|
|
||||||
|
const NewFolder = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const generating = useStore((state) => state.generating);
|
||||||
|
const setFoldersName = useStore((state) => state.setFoldersName);
|
||||||
|
|
||||||
|
const addFolder = () => {
|
||||||
|
let folderIndex = 1;
|
||||||
|
let name = `New Folder ${folderIndex}`;
|
||||||
|
|
||||||
|
while (
|
||||||
|
useStore
|
||||||
|
.getState()
|
||||||
|
.foldersName.some((_folderName) => _folderName === name)
|
||||||
|
) {
|
||||||
|
folderIndex += 1;
|
||||||
|
name = `New Folder ${folderIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFoldersName([name, ...useStore.getState().foldersName]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={`max-md:hidden flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white text-sm md:mb-2 flex-shrink-0 md:border md:border-white/20 transition-opacity ${
|
||||||
|
generating
|
||||||
|
? 'cursor-not-allowed opacity-40'
|
||||||
|
: 'cursor-pointer opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!generating) addFolder();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon />{' '}
|
||||||
|
<span className='hidden md:inline-flex text-white text-sm'>
|
||||||
|
{t('newFolder')}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewFolder;
|
|
@ -7,11 +7,15 @@ export interface ChatSlice {
|
||||||
currentChatIndex: number;
|
currentChatIndex: number;
|
||||||
generating: boolean;
|
generating: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
|
foldersName: string[];
|
||||||
|
foldersExpanded: boolean[];
|
||||||
setMessages: (messages: MessageInterface[]) => void;
|
setMessages: (messages: MessageInterface[]) => void;
|
||||||
setChats: (chats: ChatInterface[]) => void;
|
setChats: (chats: ChatInterface[]) => void;
|
||||||
setCurrentChatIndex: (currentChatIndex: number) => void;
|
setCurrentChatIndex: (currentChatIndex: number) => void;
|
||||||
setGenerating: (generating: boolean) => void;
|
setGenerating: (generating: boolean) => void;
|
||||||
setError: (error: string) => void;
|
setError: (error: string) => void;
|
||||||
|
setFoldersName: (foldersName: string[]) => void;
|
||||||
|
setFoldersExpanded: (foldersExpanded: boolean[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({
|
export const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({
|
||||||
|
@ -19,6 +23,8 @@ export const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({
|
||||||
currentChatIndex: -1,
|
currentChatIndex: -1,
|
||||||
generating: false,
|
generating: false,
|
||||||
error: '',
|
error: '',
|
||||||
|
foldersName: [],
|
||||||
|
foldersExpanded: [],
|
||||||
setMessages: (messages: MessageInterface[]) => {
|
setMessages: (messages: MessageInterface[]) => {
|
||||||
set((prev: ChatSlice) => ({
|
set((prev: ChatSlice) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
@ -49,4 +55,16 @@ export const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({
|
||||||
error: error,
|
error: error,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
setFoldersName: (foldersName: string[]) => {
|
||||||
|
set((prev: ChatSlice) => ({
|
||||||
|
...prev,
|
||||||
|
foldersName: foldersName,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
setFoldersExpanded: (foldersExpanded: boolean[]) => {
|
||||||
|
set((prev: ChatSlice) => ({
|
||||||
|
...prev,
|
||||||
|
foldersExpanded: foldersExpanded,
|
||||||
|
}));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,6 +59,8 @@ const useStore = create<StoreState>()(
|
||||||
hideMenuOptions: state.hideMenuOptions,
|
hideMenuOptions: state.hideMenuOptions,
|
||||||
firstVisit: state.firstVisit,
|
firstVisit: state.firstVisit,
|
||||||
hideSideMenu: state.hideSideMenu,
|
hideSideMenu: state.hideSideMenu,
|
||||||
|
foldersName: state.foldersName,
|
||||||
|
foldersExpanded: state.foldersExpanded,
|
||||||
}),
|
}),
|
||||||
version: 7,
|
version: 7,
|
||||||
migrate: (persistedState, version) => {
|
migrate: (persistedState, version) => {
|
||||||
|
|
|
@ -11,6 +11,7 @@ export interface MessageInterface {
|
||||||
|
|
||||||
export interface ChatInterface {
|
export interface ChatInterface {
|
||||||
title: string;
|
title: string;
|
||||||
|
folder?: string;
|
||||||
messages: MessageInterface[];
|
messages: MessageInterface[];
|
||||||
config: ConfigInterface;
|
config: ConfigInterface;
|
||||||
titleSet: boolean;
|
titleSet: boolean;
|
||||||
|
@ -25,6 +26,15 @@ export interface ConfigInterface {
|
||||||
frequency_penalty: number;
|
frequency_penalty: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatHistoryInterface {
|
||||||
|
title: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatHistoryFolderInterface {
|
||||||
|
[folderName: string]: ChatHistoryInterface[];
|
||||||
|
}
|
||||||
|
|
||||||
export type ModelOptions = 'gpt-4' | 'gpt-4-32k' | 'gpt-3.5-turbo';
|
export type ModelOptions = 'gpt-4' | 'gpt-4-32k' | 'gpt-3.5-turbo';
|
||||||
// | 'gpt-3.5-turbo-0301';
|
// | 'gpt-3.5-turbo-0301';
|
||||||
// | 'gpt-4-0314'
|
// | 'gpt-4-0314'
|
||||||
|
|
Loading…
Reference in a new issue