diff --git a/frontend/src/assets/sources.svg b/frontend/src/assets/sources.svg new file mode 100644 index 00000000..4bb7d30b --- /dev/null +++ b/frontend/src/assets/sources.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 00000000..f0f4878b --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import Exit from '../assets/exit.svg'; + +type SidebarProps = { + isOpen: boolean; + toggleState: (arg0: boolean) => void; + children: React.ReactNode; +}; + +export default function Sidebar({ + isOpen, + toggleState, + children, +}: SidebarProps) { + const sidebarRef = React.useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if ( + sidebarRef.current && + !sidebarRef.current.contains(event.target as Node) + ) { + toggleState(false); + } + }; + + React.useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + return ( +
+
+
+ +
+
+ {children} +
+
+
+ ); +} diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 4adfbf94..8f8c0ef0 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -1,17 +1,20 @@ import { forwardRef, useState } from 'react'; -import Avatar from '../components/Avatar'; -import CopyButton from '../components/CopyButton'; -import remarkGfm from 'remark-gfm'; -import { FEEDBACK, MESSAGE_TYPE } from './conversationModels'; -import classes from './ConversationBubble.module.css'; -import Alert from './../assets/alert.svg'; -import Like from './../assets/like.svg?react'; -import Dislike from './../assets/dislike.svg?react'; - import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import remarkGfm from 'remark-gfm'; + +import Alert from '../assets/alert.svg'; import DocsGPT3 from '../assets/cute_docsgpt3.svg'; +import Dislike from '../assets/dislike.svg?react'; +import Like from '../assets/like.svg?react'; +import Sources from '../assets/sources.svg'; +import Avatar from '../components/Avatar'; +import CopyButton from '../components/CopyButton'; +import Sidebar from '../components/Sidebar'; +import classes from './ConversationBubble.module.css'; +import { FEEDBACK, MESSAGE_TYPE } from './conversationModels'; + const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false; const ConversationBubble = forwardRef< @@ -35,9 +38,10 @@ const ConversationBubble = forwardRef< const [isDislikeHovered, setIsDislikeHovered] = useState(false); const [isLikeClicked, setIsLikeClicked] = useState(false); const [isDislikeClicked, setIsDislikeClicked] = useState(false); + const [activeTooltip, setActiveTooltip] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); let bubble; - if (type === 'QUESTION') { bubble = (
@@ -55,18 +59,115 @@ const ConversationBubble = forwardRef< ref={ref} className={`flex flex-wrap self-start ${className} group flex-col dark:text-bright-gray`} > -
- +
+ + } /> - } - /> - +

Sources

+
+
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ + + + + +
+ ))} +
+
+ ) : ( +
+
+ + } + /> +

Sources

+
+
+
+ {sources?.slice(0, 3)?.map((source, index) => ( +
+
setActiveTooltip(index)} + onMouseOut={() => setActiveTooltip(null)} + onClick={() => + source.source && source.source !== 'local' + ? window.open( + source.source, + '_blank', + 'noopener, noreferrer', + ) + : setOpenSource(openSource === index ? null : index) + } + > +

+ {source.title} +

+
+ {activeTooltip === index && ( +
setActiveTooltip(index)} + onMouseOut={() => setActiveTooltip(null)} + > +

+ {source.text} +

+
+ )} +
+ ))} + {(sources?.length ?? 0) > 3 && ( +
setIsSidebarOpen(true)} + > +

{`View ${ + sources?.length ? sources.length - 3 : 0 + } more`}

+
+ )} +
+
+
+ )} +
+
+ + } + /> +

Answer

+
{message} - {DisableSourceFE || - type === 'ERROR' || - !sources || - sources.length === 0 ? null : ( - <> - -
-
Sources:
-
- {sources?.map((source, index) => ( -
- source.source !== 'local' - ? window.open( - source.source, - '_blank', - 'noopener, noreferrer', - ) - : setOpenSource(openSource === index ? null : index) - } - > -

- {index + 1}. {source.title.substring(0, 45)} -

-
- ))} -
-
- - )}
-
+
)}
- - {sources && openSource !== null && sources[openSource] && ( -
-

- Source: {sources[openSource].title} -

- -
-

- {sources[openSource].text} -

-
-
+ {sources && ( + { + setIsSidebarOpen(state); + }} + children={} + /> )}
); @@ -312,4 +366,31 @@ const ConversationBubble = forwardRef< return bubble; }); +type AllSourcesProps = { + sources: { title: string; text: string; source: string }[]; +}; + +function AllSources(sources: AllSourcesProps) { + return ( +
+
+

{`${sources.sources.length} Sources`}

+
+
+
+ {sources.sources.map((source, index) => ( +
+

+ {`${index + 1}. ${source.title}`} +

+

+ {source.text} +

+
+ ))} +
+
+ ); +} + export default ConversationBubble; diff --git a/frontend/src/index.css b/frontend/src/index.css index cf90289f..3b78e9de 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -433,3 +433,11 @@ template { .bottom-safe { bottom: env(safe-area-inset-bottom, 0); } + +.ellipsis-text { + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + text-overflow: ellipsis; +}