diff --git a/public/locales/da/main.json b/public/locales/da/main.json index 6dd5af3..4e2c0b9 100644 --- a/public/locales/da/main.json +++ b/public/locales/da/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "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." - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/en/main.json b/public/locales/en/main.json index 661b8f7..b115b2a 100644 --- a/public/locales/en/main.json +++ b/public/locales/en/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "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." - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/es/main.json b/public/locales/es/main.json index a1a5f5a..3aac596 100644 --- a/public/locales/es/main.json +++ b/public/locales/es/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "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." - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/ja/main.json b/public/locales/ja/main.json index 3caa45f..79d9754 100644 --- a/public/locales/ja/main.json +++ b/public/locales/ja/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "title": "ShareGPTに投稿", "warning": "ShareGPTに会話を投稿すると、誰でもアクセスして閲覧できるようになることに注意してください。一度投稿すると、会話は非表示にできず、削除もできません。また、他の人がアーカイブや共有する可能性があります。このプラットフォームで機密性のある情報や個人情報を共有しないように注意してください。" - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/ms/main.json b/public/locales/ms/main.json index 09b7b28..d7d559c 100644 --- a/public/locales/ms/main.json +++ b/public/locales/ms/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "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." - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/nb/main.json b/public/locales/nb/main.json index 46c1780..019f017 100644 --- a/public/locales/nb/main.json +++ b/public/locales/nb/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "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." - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/sv/main.json b/public/locales/sv/main.json index 06960d7..07ae1d3 100644 --- a/public/locales/sv/main.json +++ b/public/locales/sv/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "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." - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/zh-CN/main.json b/public/locales/zh-CN/main.json index 4aaf85d..58e4ea5 100644 --- a/public/locales/zh-CN/main.json +++ b/public/locales/zh-CN/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "title": "发布至 ShareGPT", "warning": "请注意,把您的对话发布到 ShareGPT 后,任何人都可以公开访问和查看。发布后,对话不能被隐藏或删除,且可能被其他人存档或分享。建议您慎重考虑,在这个平台上避免分享敏感或私密信息。" - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/zh-HK/main.json b/public/locales/zh-HK/main.json index 8972d55..fd65f14 100644 --- a/public/locales/zh-HK/main.json +++ b/public/locales/zh-HK/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "title": "po 上 ShareGPT", "warning": "請注意,你將呢個傾偈 po 上 ShareGPT 之後,佢會係公開嘅,所有人都可以見到你寫嘅嘢。一旦 po 咗,呢個傾偈將冇得被隱藏或刪除,亦都可能畀人存檔同分享。我哋建議你諗清楚,唔好喺嗰度分享敏感或私人資料。" - } + }, + "newFolder": "New Folder" } diff --git a/public/locales/zh-TW/main.json b/public/locales/zh-TW/main.json index 899efc9..b22c90b 100644 --- a/public/locales/zh-TW/main.json +++ b/public/locales/zh-TW/main.json @@ -32,5 +32,6 @@ "postOnShareGPT": { "title": "發佈至 ShareGPT", "warning": "請注意,將您的對話發佈至 ShareGPT 後,任何人都可以公開訪問和查看。一旦發佈,對話將無法隱藏或刪除,並且可能被他人存檔或分享。我們建議您慎重考慮,並避免在此平台上分享敏感或私人信息。" - } + }, + "newFolder": "New Folder" } diff --git a/src/assets/icons/FolderIcon.tsx b/src/assets/icons/FolderIcon.tsx new file mode 100644 index 0000000..6621594 --- /dev/null +++ b/src/assets/icons/FolderIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +const FolderIcon = (props: React.SVGProps) => { + return ( + + + + ); +}; + +export default FolderIcon; diff --git a/src/components/Menu/ChatFolder.tsx b/src/components/Menu/ChatFolder.tsx new file mode 100644 index 0000000..e0e42ce --- /dev/null +++ b/src/components/Menu/ChatFolder.tsx @@ -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(null); + + const [_folderName, _setFolderName] = useState(folderName); + const [isEdit, setIsEdit] = useState(false); + const [isDelete, setIsDelete] = useState(false); + // const [isExpanded, setIsExpanded] = useState( + // useStore.getState().foldersExpanded[folderIndex] + // ); + const [isHover, setIsHover] = useState(false); + + const handleTick = (e: React.MouseEvent) => { + 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) => { + 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) => { + e.preventDefault(); + e.stopPropagation(); + setIsHover(true); + }; + + const handleDragLeave = () => { + setIsHover(false); + }; + + const toggleExpanded = () => { + const updatedFoldersExpanded = [...foldersExpanded]; + updatedFoldersExpanded[folderIndex] = !updatedFoldersExpanded[folderIndex]; + setFoldersExpanded(updatedFoldersExpanded); + }; + + return ( +
+
+ +
+ {isEdit ? ( + { + _setFolderName(e.target.value); + }} + onClick={(e) => e.stopPropagation()} + ref={inputRef} + /> + ) : ( + _folderName + )} +
+
e.stopPropagation()} + > + {isDelete || isEdit ? ( + <> + + + + ) : ( + <> + + + + + )} +
+
+
+ {foldersExpanded[folderIndex] && + folderChats.map((chat) => ( + + ))} +
+
+ ); +}; + +export default ChatFolder; diff --git a/src/components/Menu/ChatHistory.tsx b/src/components/Menu/ChatHistory.tsx new file mode 100644 index 0000000..42e73a2 --- /dev/null +++ b/src/components/Menu/ChatHistory.tsx @@ -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(false); + const [isEdit, setIsEdit] = useState(false); + const [_title, _setTitle] = useState(title); + const inputRef = useRef(null); + + const handleTick = (e: React.MouseEvent) => { + 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) => { + if (e.dataTransfer) { + e.dataTransfer.setData('chatIndex', String(chatIndex)); + } + }; + + useEffect(() => { + if (inputRef && inputRef.current) inputRef.current.focus(); + }, [isEdit]); + + return ( + { + if (!generating) setCurrentChatIndex(chatIndex); + }} + draggable + onDragStart={handleDragStart} + > + +
+ {isEdit ? ( + { + _setTitle(e.target.value); + }} + ref={inputRef} + /> + ) : ( + _title + )} + + {isEdit || ( +
+ )} +
+ {active && ( +
+ {isDelete || isEdit ? ( + <> + + + + ) : ( + <> + + + + )} +
+ )} +
+ ); + } +); + +export default ChatHistory; diff --git a/src/components/Menu/ChatHistoryList.tsx b/src/components/Menu/ChatHistoryList.tsx index 43a5750..45283ac 100644 --- a/src/components/Menu/ChatHistoryList.tsx +++ b/src/components/Menu/ChatHistoryList.tsx @@ -2,21 +2,71 @@ import React, { useEffect, useRef, useState } from 'react'; import useStore from '@store/store'; import { shallow } from 'zustand/shallow'; -import ChatIcon from '@icon/ChatIcon'; -import EditIcon from '@icon/EditIcon'; -import DeleteIcon from '@icon/DeleteIcon'; -import TickIcon from '@icon/TickIcon'; -import CrossIcon from '@icon/CrossIcon'; +import NewFolder from './NewFolder'; +import ChatFolder from './ChatFolder'; +import ChatHistory from './ChatHistory'; -import useInitialiseNewChat from '@hooks/useInitialiseNewChat'; +import { + ChatHistoryInterface, + ChatHistoryFolderInterface, + ChatInterface, +} from '@type/chat'; const ChatHistoryList = () => { const currentChatIndex = useStore((state) => state.currentChatIndex); + const setChats = useStore((state) => state.setChats); const chatTitles = useStore( (state) => state.chats?.map((chat) => chat.title), shallow ); + const [isHover, setIsHover] = useState(false); + const [folders, setFolders] = useState({}); + const [noFolders, setNoFolders] = useState([]); + const chatsRef = useRef(useStore.getState().chats || []); + const foldersNameRef = useRef(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(() => { if ( chatTitles && @@ -24,22 +74,80 @@ const ChatHistoryList = () => { currentChatIndex < chatTitles.length ) { 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]); + const handleDrop = (e: React.DragEvent) => { + 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) => { + e.preventDefault(); + setIsHover(true); + }; + + const handleDragLeave = () => { + setIsHover(false); + }; + + const handleDragEnd = () => { + setIsHover(false); + }; + return ( -
+
+
- {chatTitles && - chatTitles.map((title, index) => ( - - ))} - {/* */} + {Object.keys(folders).map((folderName, folderIndex) => ( + + ))} + {noFolders.map(({ title, index }) => ( + + ))}
+
); }; @@ -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(false); - const [isEdit, setIsEdit] = useState(false); - const [_title, _setTitle] = useState(title); - const inputRef = useRef(null); - - const handleTick = (e: React.MouseEvent) => { - 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 ( - { - if (!generating) setCurrentChatIndex(chatIndex); - }} - > - -
- {isEdit ? ( - { - _setTitle(e.target.value); - }} - ref={inputRef} - /> - ) : ( - _title - )} - - {isEdit || ( -
- )} -
- {active && ( -
- {isDelete || isEdit ? ( - <> - - - - ) : ( - <> - - - - )} -
- )} -
- ); - } -); - export default ChatHistoryList; diff --git a/src/components/Menu/NewFolder.tsx b/src/components/Menu/NewFolder.tsx new file mode 100644 index 0000000..b811e2c --- /dev/null +++ b/src/components/Menu/NewFolder.tsx @@ -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 ( + { + if (!generating) addFolder(); + }} + > + {' '} + + {t('newFolder')} + + + ); +}; + +export default NewFolder; diff --git a/src/store/chat-slice.ts b/src/store/chat-slice.ts index e2fcd54..1aec79c 100644 --- a/src/store/chat-slice.ts +++ b/src/store/chat-slice.ts @@ -7,11 +7,15 @@ export interface ChatSlice { currentChatIndex: number; generating: boolean; error: string; + foldersName: string[]; + foldersExpanded: boolean[]; setMessages: (messages: MessageInterface[]) => void; setChats: (chats: ChatInterface[]) => void; setCurrentChatIndex: (currentChatIndex: number) => void; setGenerating: (generating: boolean) => void; setError: (error: string) => void; + setFoldersName: (foldersName: string[]) => void; + setFoldersExpanded: (foldersExpanded: boolean[]) => void; } export const createChatSlice: StoreSlice = (set, get) => ({ @@ -19,6 +23,8 @@ export const createChatSlice: StoreSlice = (set, get) => ({ currentChatIndex: -1, generating: false, error: '', + foldersName: [], + foldersExpanded: [], setMessages: (messages: MessageInterface[]) => { set((prev: ChatSlice) => ({ ...prev, @@ -49,4 +55,16 @@ export const createChatSlice: StoreSlice = (set, get) => ({ error: error, })); }, + setFoldersName: (foldersName: string[]) => { + set((prev: ChatSlice) => ({ + ...prev, + foldersName: foldersName, + })); + }, + setFoldersExpanded: (foldersExpanded: boolean[]) => { + set((prev: ChatSlice) => ({ + ...prev, + foldersExpanded: foldersExpanded, + })); + }, }); diff --git a/src/store/store.ts b/src/store/store.ts index 4af3822..cc07336 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -59,6 +59,8 @@ const useStore = create()( hideMenuOptions: state.hideMenuOptions, firstVisit: state.firstVisit, hideSideMenu: state.hideSideMenu, + foldersName: state.foldersName, + foldersExpanded: state.foldersExpanded, }), version: 7, migrate: (persistedState, version) => { diff --git a/src/types/chat.ts b/src/types/chat.ts index c1d8125..d20db1f 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -11,6 +11,7 @@ export interface MessageInterface { export interface ChatInterface { title: string; + folder?: string; messages: MessageInterface[]; config: ConfigInterface; titleSet: boolean; @@ -25,6 +26,15 @@ export interface ConfigInterface { 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'; // | 'gpt-3.5-turbo-0301'; // | 'gpt-4-0314'