diff --git a/public/locales/en/main.json b/public/locales/en/main.json index 04855d11f..831f51249 100644 --- a/public/locales/en/main.json +++ b/public/locales/en/main.json @@ -1,6 +1,8 @@ { "save": "Save", "generate": "Generate", + "add_image_url": "Add image URL", + "enter_image_url_placeholder": "Enter image URL", "cancel": "Cancel", "confirm": "Confirm", "warning": "Warning", @@ -33,6 +35,7 @@ "total": "Total", "resetCost": "Reset Costs", "countTotalTokens": "Count total tokens", + "displayChatSize": "Display chat size in title", "morePrompts": "You can find more prompts here: ", "clearPrompts": "Clear prompts", "postOnShareGPT": { diff --git a/src/App.tsx b/src/App.tsx index 9f6d509f6..fe16431e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -79,10 +79,12 @@ function App() { return (
- - - - +
+ + + + +
); } diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 7ec9c8650..d5ab6a272 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -7,12 +7,12 @@ import StopGeneratingButton from '@components/StopGeneratingButton/StopGeneratin const Chat = () => { const hideSideMenu = useStore((state) => state.hideSideMenu); + const menuWidth = useStore((state) => state.menuWidth); return (
diff --git a/src/components/Chat/ChatContent/ChatTitle.tsx b/src/components/Chat/ChatContent/ChatTitle.tsx index 4130b0ee1..bfa0f1a44 100644 --- a/src/components/Chat/ChatContent/ChatTitle.tsx +++ b/src/components/Chat/ChatContent/ChatTitle.tsx @@ -43,7 +43,7 @@ const ChatTitle = React.memo(() => { return config ? ( <>
{ setIsModalOpen(true); }} diff --git a/src/components/Chat/ChatContent/Message/View/ContentView.tsx b/src/components/Chat/ChatContent/Message/View/ContentView.tsx index 11556aec5..33a12cae1 100644 --- a/src/components/Chat/ChatContent/Message/View/ContentView.tsx +++ b/src/components/Chat/ChatContent/Message/View/ContentView.tsx @@ -19,7 +19,12 @@ import CrossIcon from '@icon/CrossIcon'; import useSubmit from '@hooks/useSubmit'; -import { ChatInterface, ContentInterface, ImageContentInterface, TextContentInterface } from '@type/chat'; +import { + ChatInterface, + ContentInterface, + ImageContentInterface, + TextContentInterface, +} from '@type/chat'; import { codeLanguageSubset } from '@constants/chat'; @@ -41,7 +46,7 @@ const ContentView = memo( messageIndex, }: { role: string; - content: ContentInterface[], + content: ContentInterface[]; setIsEdit: React.Dispatch>; messageIndex: number; }) => { @@ -132,13 +137,19 @@ const ContentView = memo( {(content[0] as TextContentInterface).text} ) : ( - {(content[0] as TextContentInterface).text} + + {(content[0] as TextContentInterface).text} + )}
-
+
{(content.slice(1) as ImageContentInterface[]).map((image, index) => ( -
- {`uploaded-${index}`} +
+ {`uploaded-${index}`}
))}
diff --git a/src/components/Chat/ChatContent/Message/View/EditView.tsx b/src/components/Chat/ChatContent/Message/View/EditView.tsx index 7bfdf2830..ff5f0e85f 100644 --- a/src/components/Chat/ChatContent/Message/View/EditView.tsx +++ b/src/components/Chat/ChatContent/Message/View/EditView.tsx @@ -34,6 +34,7 @@ const EditView = ({ const [_content, _setContent] = useState(content); const [isModalOpen, setIsModalOpen] = useState(false); + const [imageUrl, setImageUrl] = useState(''); const textareaRef = React.createRef(); const { t } = useTranslation(); @@ -104,6 +105,22 @@ const EditView = ({ _setContent(updatedContent); }; + const handleImageUrlChange = () => { + if (imageUrl.trim() === '') return; + + const newImage: ImageContentInterface = { + type: 'image_url', + image_url: { + detail: 'auto', + url: imageUrl, + }, + }; + + const updatedContent = [..._content, newImage]; + _setContent(updatedContent); + setImageUrl(''); + }; + const handleImageDetailChange = (index: number, detail: string) => { const updatedImages = [..._content]; updatedImages[index + 1].image_url.detail = detail; @@ -229,6 +246,9 @@ const EditView = ({ setIsEdit={setIsEdit} _setContent={_setContent} _content={_content} + imageUrl={imageUrl} + setImageUrl={setImageUrl} + handleImageUrlChange={handleImageUrlChange} /> {isModalOpen && ( ) => void; @@ -265,6 +288,9 @@ const EditViewButtons = memo( setIsEdit: React.Dispatch>; _setContent: React.Dispatch>; _content: ContentInterface[]; + imageUrl: string; + setImageUrl: React.Dispatch>; + handleImageUrlChange: () => void; }) => { const { t } = useTranslation(); const generating = useStore.getState().generating; @@ -286,65 +312,82 @@ const EditViewButtons = memo( return (
{modelTypes[model] == 'image' && ( -
-
- {_content.slice(1).map((image, index) => ( -
- {`uploaded-${index}`} -
- - + <> +
+
+ {_content.slice(1).map((image, index) => ( +
+ {`uploaded-${index}`} +
+ + +
-
- ))} + ))} + +
+
+
+ setImageUrl(e.target.value)} + placeholder={t('enter_image_url_placeholder') as string} + className='input input-bordered w-full max-w-xs text-gray-800 dark:text-white p-3 border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-10 focus:outline-none' + />
- {/* Hidden file input */} -
+ )} +
{sticky && ( diff --git a/src/components/Menu/ChatFolder.tsx b/src/components/Menu/ChatFolder.tsx index 42b144c29..18e5ae5a2 100644 --- a/src/components/Menu/ChatFolder.tsx +++ b/src/components/Menu/ChatFolder.tsx @@ -25,9 +25,17 @@ import useHideOnOutsideClick from '@hooks/useHideOnOutsideClick'; const ChatFolder = ({ folderChats, folderId, + selectedChats, + setSelectedChats, + lastSelectedIndex, + setLastSelectedIndex, }: { folderChats: ChatHistoryInterface[]; folderId: string; + selectedChats: number[]; + setSelectedChats: (indices: number[]) => void; + lastSelectedIndex: number | null; + setLastSelectedIndex: (index: number) => void; }) => { const folderName = useStore((state) => state.folders[folderId]?.name); const isExpanded = useStore((state) => state.folders[folderId]?.expanded); @@ -116,12 +124,15 @@ const ChatFolder = ({ setFolders(updatedFolders); // update chat folderId to new folderId - const chatIndex = Number(e.dataTransfer.getData('chatIndex')); + const chatIndices = JSON.parse(e.dataTransfer.getData('chatIndices')); const updatedChats: ChatInterface[] = JSON.parse( JSON.stringify(useStore.getState().chats) ); - updatedChats[chatIndex].folder = folderId; + chatIndices.forEach((chatIndex: number) => { + updatedChats[chatIndex].folder = folderId; + }); setChats(updatedChats); + setSelectedChats([]); } }; @@ -304,7 +315,12 @@ const ChatFolder = ({ ))}
diff --git a/src/components/Menu/ChatHistory.tsx b/src/components/Menu/ChatHistory.tsx index f36eff6af..354cd211f 100644 --- a/src/components/Menu/ChatHistory.tsx +++ b/src/components/Menu/ChatHistory.tsx @@ -8,6 +8,7 @@ import DeleteIcon from '@icon/DeleteIcon'; import EditIcon from '@icon/EditIcon'; import TickIcon from '@icon/TickIcon'; import useStore from '@store/store'; +import { formatNumber } from '@utils/chat'; const ChatHistoryClass = { normal: @@ -21,7 +22,23 @@ const ChatHistoryClass = { }; const ChatHistory = React.memo( - ({ title, chatIndex }: { title: string; chatIndex: number }) => { + ({ + title, + chatIndex, + chatSize, + selectedChats, + setSelectedChats, + lastSelectedIndex, + setLastSelectedIndex, + }: { + title: string; + chatIndex: number; + chatSize?: number; + selectedChats: number[]; + setSelectedChats: (indices: number[]) => void; + lastSelectedIndex: number | null; + setLastSelectedIndex: (index: number) => void; + }) => { const initialiseNewChat = useInitialiseNewChat(); const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex); const setChats = useStore((state) => state.setChats); @@ -46,7 +63,13 @@ const ChatHistory = React.memo( const updatedChats = JSON.parse( JSON.stringify(useStore.getState().chats) ); - updatedChats.splice(chatIndex, 1); + const indicesToDelete = + selectedChats.length > 0 ? selectedChats : [chatIndex]; + indicesToDelete + .sort((a, b) => b - a) + .forEach((index) => { + updatedChats.splice(index, 1); + }); if (updatedChats.length > 0) { setCurrentChatIndex(0); setChats(updatedChats); @@ -54,6 +77,7 @@ const ChatHistory = React.memo( initialiseNewChat(); } setIsDelete(false); + setSelectedChats([]); }; const handleKeyDown = (e: React.KeyboardEvent) => { @@ -77,7 +101,32 @@ const ChatHistory = React.memo( const handleDragStart = (e: React.DragEvent) => { if (e.dataTransfer) { - e.dataTransfer.setData('chatIndex', String(chatIndex)); + const chatIndices = + selectedChats.length > 0 ? selectedChats : [chatIndex]; + e.dataTransfer.setData('chatIndices', JSON.stringify(chatIndices)); + } + }; + + const handleCheckboxClick = (e: React.MouseEvent) => { + if (e.shiftKey && lastSelectedIndex !== null) { + const start = Math.min(lastSelectedIndex, chatIndex); + const end = Math.max(lastSelectedIndex, chatIndex); + const newSelectedChats = [...selectedChats]; + for (let i = start; i <= end; i++) { + if (!newSelectedChats.includes(i)) { + newSelectedChats.push(i); + } + } + setSelectedChats(newSelectedChats); + } else { + if (selectedChats.includes(chatIndex)) { + setSelectedChats( + selectedChats.filter((index) => index !== chatIndex) + ); + } else { + setSelectedChats([...selectedChats, chatIndex]); + } + setLastSelectedIndex(chatIndex); } }; @@ -93,15 +142,24 @@ const ChatHistory = React.memo( generating ? 'cursor-not-allowed opacity-40' : 'cursor-pointer opacity-100' - }`} + } ${selectedChats.includes(chatIndex) ? 'bg-blue-500' : ''}`} onClick={() => { if (!generating) setCurrentChatIndex(chatIndex); }} draggable onDragStart={handleDragStart} > + {}} + /> -
+
{isEdit ? ( ) : ( - _title + `${title}${chatSize ? ` (${formatNumber(chatSize)})` : ''}` )} {isEdit || ( diff --git a/src/components/Menu/ChatHistoryList.tsx b/src/components/Menu/ChatHistoryList.tsx index 42a9757dd..04fe2b432 100644 --- a/src/components/Menu/ChatHistoryList.tsx +++ b/src/components/Menu/ChatHistoryList.tsx @@ -11,10 +11,13 @@ import { ChatHistoryFolderInterface, ChatInterface, FolderCollection, + isImageContent, + isTextContent, } from '@type/chat'; const ChatHistoryList = () => { const currentChatIndex = useStore((state) => state.currentChatIndex); + const displayChatSize = useStore((state) => state.displayChatSize); const setChats = useStore((state) => state.setChats); const setFolders = useStore((state) => state.setFolders); const chatTitles = useStore( @@ -30,6 +33,10 @@ const ChatHistoryList = () => { [] ); const [filter, setFilter] = useState(''); + const [selectedChats, setSelectedChats] = useState([]); + const [lastSelectedIndex, setLastSelectedIndex] = useState( + null + ); const chatsRef = useRef(useStore.getState().chats || []); const foldersRef = useRef(useStore.getState().folders); @@ -40,6 +47,7 @@ const ChatHistoryList = () => { const _noFolders: ChatHistoryInterface[] = []; const chats = useStore.getState().chats; const folders = useStore.getState().folders; + const displayChatSize = useStore.getState().displayChatSize; Object.values(folders) .sort((a, b) => a.order - b.order) @@ -61,13 +69,51 @@ const ChatHistoryList = () => { return; if (!chat.folder) { - _noFolders.push({ title: chat.title, index: index, id: chat.id }); + _noFolders.push({ + title: chat.title, + index: index, + id: chat.id, + chatSize: !displayChatSize + ? undefined + : chat.messages.reduce( + (prev, current) => + prev + + current.content.reduce( + (prevInner, currCont) => + prevInner + + (isTextContent(currCont) + ? currCont.text.length + : isImageContent(currCont) + ? currCont.image_url.url.length + : 0), + 0 + ), + 0 + ), + }); } else { if (!_folders[chat.folder]) _folders[_chatFolderName] = []; _folders[chat.folder].push({ title: chat.title, index: index, id: chat.id, + chatSize: !displayChatSize + ? undefined + : chat.messages.reduce( + (prev, current) => + prev + + current.content.reduce( + (prevInner, currCont) => + prevInner + + (isTextContent(currCont) + ? currCont.text.length + : isImageContent(currCont) + ? currCont.image_url.url.length + : 0), + 0 + ), + 0 + ), }); } }); @@ -80,7 +126,7 @@ const ChatHistoryList = () => { useEffect(() => { updateFolders(); - useStore.subscribe((state) => { + const unsubscribe = useStore.subscribe((state) => { if ( !state.generating && state.chats && @@ -93,8 +139,15 @@ const ChatHistoryList = () => { foldersRef.current = state.folders; } }); + return () => { + unsubscribe(); + }; }, []); + useEffect(() => { + updateFolders(); + }, [displayChatSize]); + useEffect(() => { if ( chatTitles && @@ -131,12 +184,15 @@ const ChatHistoryList = () => { e.stopPropagation(); setIsHover(false); - const chatIndex = Number(e.dataTransfer.getData('chatIndex')); + const chatIndices = JSON.parse(e.dataTransfer.getData('chatIndices')); const updatedChats: ChatInterface[] = JSON.parse( JSON.stringify(useStore.getState().chats) ); - delete updatedChats[chatIndex].folder; + chatIndices.forEach((chatIndex: number) => { + delete updatedChats[chatIndex].folder; + }); setChats(updatedChats); + setSelectedChats([]); } }; @@ -170,10 +226,23 @@ const ChatHistoryList = () => { folderChats={chatFolders[folderId]} folderId={folderId} key={folderId} + selectedChats={selectedChats} + setSelectedChats={setSelectedChats} + lastSelectedIndex={lastSelectedIndex} + setLastSelectedIndex={setLastSelectedIndex} /> ))} - {noChatFolders.map(({ title, index, id }) => ( - + {noChatFolders.map(({ title, index, id, chatSize }) => ( + ))}
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index d808368dc..7f124640b 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef } from 'react'; - import useStore from '@store/store'; import NewChat from './NewChat'; @@ -14,8 +13,11 @@ import MenuIcon from '@icon/MenuIcon'; const Menu = () => { const hideSideMenu = useStore((state) => state.hideSideMenu); const setHideSideMenu = useStore((state) => state.setHideSideMenu); + const menuWidth = useStore((state) => state.menuWidth); + const setMenuWidth = useStore((state) => state.setMenuWidth); const windowWidthRef = useRef(window.innerWidth); + const isResizing = useRef(false); useEffect(() => { if (window.innerWidth < 768) setHideSideMenu(true); @@ -28,13 +30,41 @@ const Menu = () => { }); }, []); + const handleMouseDown = () => { + isResizing.current = true; + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isResizing.current) { + const newWidth = e.clientX; + if (newWidth > 100 && newWidth < window.innerWidth * 0.75) { + setMenuWidth(newWidth); + } + } + }; + + const handleMouseUp = () => { + isResizing.current = false; + }; + + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, []); + return ( <>