mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-30 09:03:15 +00:00
conversation tile: add menu, add share modal
This commit is contained in:
3
frontend/src/assets/red-trash.svg
Normal file
3
frontend/src/assets/red-trash.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.66427 17.7747C6.66427 18.2167 6.84488 18.6406 7.16637 18.9532C7.48787 19.2658 7.9239 19.4413 8.37856 19.4413H15.2357C15.6904 19.4413 16.1264 19.2658 16.4479 18.9532C16.7694 18.6406 16.95 18.2167 16.95 17.7747V7.77468H6.66427V17.7747ZM8.37856 9.44135H15.2357V17.7747H8.37856V9.44135ZM14.8071 5.27468L13.95 4.44135H9.66427L8.80713 5.27468H5.80713V6.94135H17.8071V5.27468H14.8071Z" fill="#D30000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
3
frontend/src/assets/share.svg
Normal file
3
frontend/src/assets/share.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="17" viewBox="0 0 14 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.04167 7.2997C1.96431 7.2997 1.89013 7.32976 1.83543 7.38326C1.78073 7.43677 1.75 7.50934 1.75 7.585V15.0029C1.75 15.1604 1.88067 15.2882 2.04167 15.2882H11.9583C12.0357 15.2882 12.1099 15.2581 12.1646 15.2046C12.2193 15.1511 12.25 15.0785 12.25 15.0029V7.585C12.25 7.50934 12.2193 7.43677 12.1646 7.38326C12.1099 7.32976 12.0357 7.2997 11.9583 7.2997H10.7917C10.5596 7.2997 10.337 7.20952 10.1729 7.04901C10.0089 6.8885 9.91667 6.67079 9.91667 6.44379C9.91667 6.21679 10.0089 5.99909 10.1729 5.83857C10.337 5.67806 10.5596 5.58788 10.7917 5.58788H11.9583C13.0853 5.58788 14 6.48259 14 7.585V15.0029C14 15.5325 13.7849 16.0405 13.402 16.4151C13.0191 16.7896 12.4998 17 11.9583 17H2.04167C1.50018 17 0.980877 16.7896 0.59799 16.4151C0.215104 16.0405 0 15.5325 0 15.0029V7.585C0 6.48259 0.914667 5.58788 2.04167 5.58788H3.20833C3.4404 5.58788 3.66296 5.67806 3.82705 5.83857C3.99115 5.99909 4.08333 6.21679 4.08333 6.44379C4.08333 6.67079 3.99115 6.8885 3.82705 7.04901C3.66296 7.20952 3.4404 7.2997 3.20833 7.2997H2.04167ZM6.7935 0.0838185C6.82059 0.0572492 6.85278 0.0361694 6.88821 0.0217864C6.92365 0.0074035 6.96164 0 7 0C7.03836 0 7.07635 0.0074035 7.11179 0.0217864C7.14722 0.0361694 7.17941 0.0572492 7.2065 0.0838185L10.5852 3.38877C10.6261 3.42867 10.6539 3.47955 10.6652 3.53496C10.6765 3.59037 10.6707 3.64782 10.6486 3.70001C10.6265 3.75221 10.589 3.7968 10.541 3.82815C10.4929 3.85949 10.4364 3.87617 10.3787 3.87607H7.875V10.438C7.875 10.665 7.78281 10.8827 7.61872 11.0433C7.45462 11.2038 7.23206 11.2939 7 11.2939C6.76794 11.2939 6.54538 11.2038 6.38128 11.0433C6.21719 10.8827 6.125 10.665 6.125 10.438V3.87607H3.62133C3.56357 3.87617 3.50708 3.85949 3.45902 3.82815C3.41096 3.7968 3.37349 3.75221 3.35138 3.70001C3.32926 3.64782 3.32348 3.59037 3.33478 3.53496C3.34607 3.47955 3.37394 3.42867 3.41483 3.38877L6.7935 0.0838185Z" fill="#747474"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
3
frontend/src/assets/three-dots.svg
Normal file
3
frontend/src/assets/three-dots.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="25" viewBox="0 0 10 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 3.5C3.9 3.5 3 4.4 3 5.5C3 6.6 3.9 7.5 5 7.5C6.1 7.5 7 6.6 7 5.5C7 4.4 6.1 3.5 5 3.5ZM5 17.5C3.9 17.5 3 18.4 3 19.5C3 20.6 3.9 21.5 5 21.5C6.1 21.5 7 20.6 7 19.5C7 18.4 6.1 17.5 5 17.5ZM5 10.5C3.9 10.5 3 11.4 3 12.5C3 13.6 3.9 14.5 5 14.5C6.1 14.5 7 13.6 7 12.5C7 11.4 6.1 10.5 5 10.5Z" fill="#747474"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -5,11 +5,14 @@ import Exit from '../assets/exit.svg';
|
||||
import Message from '../assets/message.svg';
|
||||
import MessageDark from '../assets/message-dark.svg';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import CheckMark2 from '../assets/checkMark2.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import Share from '../assets/share.svg';
|
||||
import threeDots from '../assets/three-dots.svg';
|
||||
import { selectConversationId } from '../preferences/preferenceSlice';
|
||||
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||
interface ConversationProps {
|
||||
name: string;
|
||||
id: string;
|
||||
@@ -32,13 +35,18 @@ export default function ConversationTile({
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [conversationName, setConversationsName] = useState('');
|
||||
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const [deleteModalState, setDeleteModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
setConversationsName(conversation.name);
|
||||
}, [conversation.name]);
|
||||
|
||||
function handleEditConversation() {
|
||||
setIsEdit(true);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
function handleSaveConversation(changedConversation: ConversationProps) {
|
||||
@@ -50,6 +58,18 @@ export default function ConversationTile({
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
function onClear() {
|
||||
setConversationsName(conversation.name);
|
||||
setIsEdit(false);
|
||||
@@ -79,7 +99,7 @@ export default function ConversationTile({
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
className="h-6 w-full px-1 text-sm font-normal leading-6 outline-[#0075FF] focus:outline-1"
|
||||
className="h-6 w-full bg-transparent px-1 text-sm font-normal leading-6 focus:outline-[#0075FF]"
|
||||
value={conversationName}
|
||||
onChange={(e) => setConversationsName(e.target.value)}
|
||||
/>
|
||||
@@ -90,36 +110,108 @@ export default function ConversationTile({
|
||||
)}
|
||||
</div>
|
||||
{conversationId === conversation.id && (
|
||||
<div className="flex text-white dark:text-[#949494]">
|
||||
<img
|
||||
src={isEdit ? CheckMark2 : Edit}
|
||||
alt="Edit"
|
||||
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
isEdit
|
||||
? handleSaveConversation({
|
||||
<div className="flex text-white dark:text-[#949494]" ref={menuRef}>
|
||||
{isEdit ? (
|
||||
<div className="flex gap-1">
|
||||
<img
|
||||
src={CheckMark2}
|
||||
alt="Edit"
|
||||
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleSaveConversation({
|
||||
id: conversationId,
|
||||
name: conversationName,
|
||||
})
|
||||
: handleEditConversation();
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={isEdit ? Exit : Trash}
|
||||
alt="Exit"
|
||||
className={`mr-4 ${
|
||||
isEdit ? 'h-3 w-3' : 'h-4 w-4'
|
||||
}mt-px cursor-pointer hover:opacity-50`}
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
isEdit ? onClear() : onDeleteConversation(conversation.id);
|
||||
}}
|
||||
/>
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={isEdit ? Exit : Trash}
|
||||
alt="Exit"
|
||||
className={`mr-4 mt-px h-3 w-3 cursor-pointer hover:opacity-50`}
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => setOpen(!isOpen)}>
|
||||
<img src={threeDots} className="mr-4 w-2" />
|
||||
</button>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div className="flex-start absolute flex w-32 translate-x-1 translate-y-5 flex-col rounded-xl bg-stone-100 text-sm text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver md:w-36">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="flex-start flex items-center gap-4 rounded-t-xl p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
>
|
||||
<img
|
||||
src={Share}
|
||||
alt="Share"
|
||||
width={14}
|
||||
height={14}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
/>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
handleEditConversation();
|
||||
}}
|
||||
className="flex-start flex items-center gap-4 p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
>
|
||||
<img
|
||||
src={Edit}
|
||||
alt="Edit"
|
||||
width={16}
|
||||
height={16}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
/>
|
||||
<span>Rename</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
setDeleteModalState('ACTIVE');
|
||||
setOpen(false);
|
||||
}}
|
||||
className="flex-start flex items-center gap-3 rounded-b-xl p-2 text-red-700 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Edit"
|
||||
width={24}
|
||||
height={24}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
/>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
message={`Are you sure you want to delete this conversation?`}
|
||||
modalState={deleteModalState}
|
||||
setModalState={setDeleteModalState}
|
||||
handleSubmit={() => onDeleteConversation(conversation.id)}
|
||||
submitLabel="Delete"
|
||||
/>
|
||||
{isShareModalOpen && conversationId && (
|
||||
<ShareConversationModal
|
||||
close={() => {
|
||||
setShareModalState(false);
|
||||
}}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
93
frontend/src/modals/ShareConversationModal.tsx
Normal file
93
frontend/src/modals/ShareConversationModal.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import Exit from '../assets/exit.svg';
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
|
||||
export const ShareConversationModal = ({
|
||||
close,
|
||||
conversationId,
|
||||
}: {
|
||||
close: () => void;
|
||||
conversationId: string;
|
||||
}) => {
|
||||
const [identifier, setIdentifier] = useState<null | string>(null);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
|
||||
const [status, setStatus] = useState<StatusType>('idle');
|
||||
const { t } = useTranslation();
|
||||
const domain = window.location.origin;
|
||||
const handleCopyKey = (url: string) => {
|
||||
navigator.clipboard.writeText(url);
|
||||
setIsCopied(true);
|
||||
};
|
||||
const shareCoversationPublicly: (isPromptable: boolean) => void = (
|
||||
isPromptable = false,
|
||||
) => {
|
||||
setStatus('loading');
|
||||
fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ conversation_id: conversationId }),
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.success && data.identifier) {
|
||||
setIdentifier(data.identifier);
|
||||
setStatus('fetched');
|
||||
} else setStatus('failed');
|
||||
})
|
||||
.catch((err) => setStatus('failed'));
|
||||
};
|
||||
return (
|
||||
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50 text-chinese-black dark:text-silver">
|
||||
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
|
||||
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-xl font-medium">Create a public page to share</h2>
|
||||
<p className="text-sm">
|
||||
Source document, personal information and further conversation will
|
||||
remain private
|
||||
</p>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 p-3 shadow-inner">{`${domain}/shared/${
|
||||
identifier ?? '....'
|
||||
}`}</span>
|
||||
{status === 'fetched' ? (
|
||||
<button
|
||||
className="my-1 h-10 w-36 rounded-full border border-solid border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
|
||||
onClick={() => handleCopyKey(`${domain}/shared/${identifier}`)}
|
||||
>
|
||||
{isCopied
|
||||
? t('modals.saveKey.copied')
|
||||
: t('modals.saveKey.copy')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="my-1 flex h-10 w-36 items-center justify-evenly rounded-full border border-solid border-purple-30 p-2 text-center text-sm font-bold text-purple-30 hover:bg-purple-30 hover:text-white"
|
||||
onClick={() => {
|
||||
shareCoversationPublicly(false);
|
||||
}}
|
||||
>
|
||||
Create
|
||||
{status === 'loading' && (
|
||||
<img
|
||||
src={Spinner}
|
||||
className="inline animate-spin cursor-pointer bg-transparent filter dark:invert"
|
||||
></img>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user