mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 16:43:16 +00:00
feat: new sources section + sidebar component
This commit is contained in:
25
frontend/src/assets/sources.svg
Normal file
25
frontend/src/assets/sources.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg width="24" height="27" viewBox="0 0 24 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.73517 17.2121L0.538918 17.9125C0.3753 18.0084 0.239616 18.1455 0.145332 18.31C0.0510491 18.4746 0.00144577 18.661 0.00144577 18.8506C0.00144577 19.0403 0.0510491 19.2267 0.145332 19.3912C0.239616 19.5558 0.3753 19.6929 0.538918 19.7888L11.0514 25.9513C11.2181 26.0489 11.4078 26.1004 11.601 26.1004C11.7941 26.1004 11.9838 26.0489 12.1505 25.9513L22.663 19.7888C22.8266 19.6929 22.9623 19.5558 23.0566 19.3912C23.1509 19.2267 23.2005 19.0403 23.2005 18.8506C23.2005 18.661 23.1509 18.4746 23.0566 18.31C22.9623 18.1455 22.8266 18.0084 22.663 17.9125L21.4668 17.2107L13.2511 22.0276C12.7506 22.321 12.1811 22.4757 11.601 22.4757C11.0209 22.4757 10.4513 22.321 9.95087 22.0276L1.73517 17.2121Z" fill="url(#paint0_linear_4013_8178)"/>
|
||||
<path d="M1.73517 11.4121L0.538918 12.1124C0.3753 12.2084 0.239616 12.3454 0.145332 12.51C0.0510491 12.6746 0.00144577 12.8609 0.00144577 13.0506C0.00144577 13.2403 0.0510491 13.4266 0.145332 13.5912C0.239616 13.7558 0.3753 13.8928 0.538918 13.9887L11.0514 20.1512C11.2181 20.2489 11.4078 20.3003 11.601 20.3003C11.7941 20.3003 11.9838 20.2489 12.1505 20.1512L22.663 13.9887C22.8266 13.8928 22.9623 13.7558 23.0566 13.5912C23.1509 13.4266 23.2005 13.2403 23.2005 13.0506C23.2005 12.8609 23.1509 12.6746 23.0566 12.51C22.9623 12.3454 22.8266 12.2084 22.663 12.1124L21.4668 11.4106L13.2511 16.2275C12.7506 16.5209 12.1811 16.6756 11.601 16.6756C11.0209 16.6756 10.4513 16.5209 9.95087 16.2275L1.73517 11.4121Z" fill="url(#paint1_linear_4013_8178)"/>
|
||||
<path d="M12.152 0.149921C11.9849 0.0517579 11.7947 0 11.601 0C11.4072 0 11.217 0.0517579 11.05 0.149921L0.537472 6.31242C0.373854 6.40835 0.23817 6.5454 0.143887 6.70997C0.0496035 6.87454 0 7.06091 0 7.25057C0 7.44024 0.0496035 7.6266 0.143887 7.79117C0.23817 7.95574 0.373854 8.09279 0.537472 8.18872L11.05 14.3512C11.217 14.4494 11.4072 14.5011 11.601 14.5011C11.7947 14.5011 11.9849 14.4494 12.152 14.3512L22.6645 8.18872C22.8281 8.09279 22.9638 7.95574 23.0581 7.79117C23.1523 7.6266 23.2019 7.44024 23.2019 7.25057C23.2019 7.06091 23.1523 6.87454 23.0581 6.70997C22.9638 6.5454 22.8281 6.40835 22.6645 6.31242L12.152 0.149921Z" fill="url(#paint2_linear_4013_8178)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4013_8178" x1="0.00144577" y1="21.6555" x2="23.2005" y2="21.6555" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#70FDF7"/>
|
||||
<stop offset="0.325" stop-color="#747696"/>
|
||||
<stop offset="0.68" stop-color="#BD5372"/>
|
||||
<stop offset="1" stop-color="#F5A06C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4013_8178" x1="0.00144577" y1="15.8555" x2="23.2005" y2="15.8555" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#70FDF7"/>
|
||||
<stop offset="0.325" stop-color="#747696"/>
|
||||
<stop offset="0.68" stop-color="#BD5372"/>
|
||||
<stop offset="1" stop-color="#F5A06C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4013_8178" x1="0" y1="7.25057" x2="23.2019" y2="7.25057" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#70FDF7"/>
|
||||
<stop offset="0.325" stop-color="#747696"/>
|
||||
<stop offset="0.68" stop-color="#BD5372"/>
|
||||
<stop offset="1" stop-color="#F5A06C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
54
frontend/src/components/Sidebar.tsx
Normal file
54
frontend/src/components/Sidebar.tsx
Normal file
@@ -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<HTMLDivElement>(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 (
|
||||
<div ref={sidebarRef} className="h-vh relative">
|
||||
<div
|
||||
className={`fixed right-0 top-0 z-50 h-full w-72 transform bg-white shadow-xl transition-all duration-300 dark:bg-chinese-black sm:w-96 ${
|
||||
isOpen ? 'translate-x-[10px]' : 'translate-x-full'
|
||||
} border-l border-[#9ca3af]/10`}
|
||||
>
|
||||
<div className="flex w-full flex-row items-end justify-end px-4 pt-3">
|
||||
<button
|
||||
className="w-7 rounded-full p-2 hover:bg-gray-1000 hover:dark:bg-gun-metal"
|
||||
onClick={() => toggleState(!isOpen)}
|
||||
>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-full flex-col items-center gap-2 py-4 px-6 text-center">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<number | null>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
|
||||
|
||||
let bubble;
|
||||
|
||||
if (type === 'QUESTION') {
|
||||
bubble = (
|
||||
<div ref={ref} className={`flex flex-row-reverse self-end ${className}`}>
|
||||
@@ -55,18 +59,115 @@ const ConversationBubble = forwardRef<
|
||||
ref={ref}
|
||||
className={`flex flex-wrap self-start ${className} group flex-col dark:text-bright-gray`}
|
||||
>
|
||||
<div className="flex flex-wrap self-start lg:flex-nowrap">
|
||||
<Avatar
|
||||
className="mt-2 h-12 w-12 text-2xl"
|
||||
avatar={
|
||||
<img
|
||||
src={DocsGPT3}
|
||||
alt="DocsGPT"
|
||||
className="h-full w-full object-cover"
|
||||
{DisableSourceFE || type === 'ERROR' ? null : !sources ||
|
||||
sources.length === 0 ? (
|
||||
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-[38px] w-[42px] text-xl"
|
||||
avatar={
|
||||
<img
|
||||
src={Sources}
|
||||
alt="Sources"
|
||||
className="h-full w-full object-fill"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<p className="text-lg font-semibold">Sources</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex h-24 cursor-pointer flex-col items-start gap-1 rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
|
||||
>
|
||||
<span className="h-px w-10 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-24 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-16 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-32 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-24 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-[38px] w-[42px] text-xl"
|
||||
avatar={
|
||||
<img
|
||||
src={Sources}
|
||||
alt="Sources"
|
||||
className="h-full w-full object-fill"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<p className="text-lg font-semibold">Sources</p>
|
||||
</div>
|
||||
<div className="ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
{sources?.slice(0, 3)?.map((source, index) => (
|
||||
<div key={index} className="relative">
|
||||
<div
|
||||
className="h-24 cursor-pointer rounded-[20px] bg-gray-1000 p-4 hover:bg-[#F1F1F1] dark:bg-gun-metal dark:hover:bg-[#2C2E3C]"
|
||||
onMouseOver={() => setActiveTooltip(index)}
|
||||
onMouseOut={() => setActiveTooltip(null)}
|
||||
onClick={() =>
|
||||
source.source && source.source !== 'local'
|
||||
? window.open(
|
||||
source.source,
|
||||
'_blank',
|
||||
'noopener, noreferrer',
|
||||
)
|
||||
: setOpenSource(openSource === index ? null : index)
|
||||
}
|
||||
>
|
||||
<p className="ellipsis-text h-22 break-words text-xs">
|
||||
{source.title}
|
||||
</p>
|
||||
</div>
|
||||
{activeTooltip === index && (
|
||||
<div
|
||||
className={`absolute left-1/2 z-30 max-h-48 w-40 translate-x-[-50%] translate-y-[3px] rounded-xl bg-[#FBFBFB] p-4 text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver sm:w-56`}
|
||||
onMouseOver={() => setActiveTooltip(index)}
|
||||
onMouseOut={() => setActiveTooltip(null)}
|
||||
>
|
||||
<p className="max-h-[164px] overflow-y-auto break-words rounded-md text-sm">
|
||||
{source.text}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(sources?.length ?? 0) > 3 && (
|
||||
<div
|
||||
className="flex h-24 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
<p className="ellipsis-text h-22 text-xs">{`View ${
|
||||
sources?.length ? sources.length - 3 : 0
|
||||
} more`}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-12 w-12 text-2xl"
|
||||
avatar={
|
||||
<img
|
||||
src={DocsGPT3}
|
||||
alt="DocsGPT"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<p className="text-lg font-semibold">Answer</p>
|
||||
</div>
|
||||
<div
|
||||
className={`ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
|
||||
type === 'ERROR'
|
||||
@@ -165,51 +266,9 @@ const ConversationBubble = forwardRef<
|
||||
>
|
||||
{message}
|
||||
</ReactMarkdown>
|
||||
{DisableSourceFE ||
|
||||
type === 'ERROR' ||
|
||||
!sources ||
|
||||
sources.length === 0 ? null : (
|
||||
<>
|
||||
<span className="mt-3 h-px w-full bg-[#DEDEDE]"></span>
|
||||
<div className="mt-3 flex w-full flex-row flex-wrap items-center justify-start gap-2">
|
||||
<div className="py-1 text-base font-semibold">Sources:</div>
|
||||
<div className="flex flex-row flex-wrap items-center justify-start gap-2">
|
||||
{sources?.map((source, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`max-w-xs cursor-pointer rounded-[28px] px-4 py-1 sm:max-w-sm md:max-w-md ${
|
||||
openSource === index
|
||||
? 'bg-[#007DFF]'
|
||||
: 'bg-[#D7EBFD] hover:bg-[#BFE1FF]'
|
||||
}`}
|
||||
onClick={() =>
|
||||
source.source !== 'local'
|
||||
? window.open(
|
||||
source.source,
|
||||
'_blank',
|
||||
'noopener, noreferrer',
|
||||
)
|
||||
: setOpenSource(openSource === index ? null : index)
|
||||
}
|
||||
>
|
||||
<p
|
||||
className={`truncate text-center text-base font-medium ${
|
||||
openSource === index
|
||||
? 'text-white'
|
||||
: 'text-[#007DFF]'
|
||||
}`}
|
||||
>
|
||||
{index + 1}. {source.title.substring(0, 45)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2 flex justify-start lg:ml-12">
|
||||
<div className="my-2 ml-2 flex justify-start">
|
||||
<div
|
||||
className={`relative mr-5 block items-center justify-center lg:invisible
|
||||
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
|
||||
@@ -292,19 +351,14 @@ const ConversationBubble = forwardRef<
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sources && openSource !== null && sources[openSource] && (
|
||||
<div className="ml-10 mt-12 max-w-[300px] break-words rounded-xl bg-blue-200 p-2 dark:bg-gun-metal sm:max-w-[800px] lg:mt-2">
|
||||
<p className="m-1 w-3/4 truncate text-xs text-gray-500 dark:text-bright-gray">
|
||||
Source: {sources[openSource].title}
|
||||
</p>
|
||||
|
||||
<div className="m-2 rounded-xl border-2 border-gray-200 bg-white p-2 dark:border-chinese-silver dark:bg-dark-charcoal">
|
||||
<p className="text-break text-black dark:text-bright-gray">
|
||||
{sources[openSource].text}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{sources && (
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
toggleState={(state: boolean) => {
|
||||
setIsSidebarOpen(state);
|
||||
}}
|
||||
children={<AllSources sources={sources} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -312,4 +366,31 @@ const ConversationBubble = forwardRef<
|
||||
return bubble;
|
||||
});
|
||||
|
||||
type AllSourcesProps = {
|
||||
sources: { title: string; text: string; source: string }[];
|
||||
};
|
||||
|
||||
function AllSources(sources: AllSourcesProps) {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="w-full">
|
||||
<p className="text-left text-xl">{`${sources.sources.length} Sources`}</p>
|
||||
<div className="mx-1 mt-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%] "></div>
|
||||
</div>
|
||||
<div className="mt-6 flex h-[90%] w-60 flex-col items-center gap-4 overflow-y-auto sm:w-80">
|
||||
{sources.sources.map((source, index) => (
|
||||
<div className="min-h-32 w-full rounded-[20px] bg-gray-1000 p-4 dark:bg-[#28292E]">
|
||||
<p className="ellipsis-text break-words text-left text-sm font-semibold">
|
||||
{`${index + 1}. ${source.title}`}
|
||||
</p>
|
||||
<p className="mt-3 max-h-24 overflow-y-auto break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
|
||||
{source.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConversationBubble;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user