diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx index b128728240031188d682cbc9ec2f55af317b1275..3b67abf39a08cea9640b7d62be0a8b98e33b35cb 100644 --- a/frontend/src/components/Sidebar/index.jsx +++ b/frontend/src/components/Sidebar/index.jsx @@ -129,7 +129,7 @@ export default function Sidebar() { </div> <a href={paths.mailToMintplex()} - className="transition-all duration-300 text-xs text-slate-200 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400" + className="transition-all duration-300 text-xs text-slate-500 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400" > @MintplexLabs </a> @@ -295,7 +295,7 @@ export function SidebarMobileHeader() { </div> <a href={paths.mailToMintplex()} - className="transition-all duration-300 text-xs text-slate-200 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400" + className="transition-all duration-300 text-xs text-slate-500 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400" > @MintplexLabs </a> diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9ffe97138dd82d489c011ac443251d2de69e87ff --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -0,0 +1,99 @@ +import { memo, useState } from "react"; +import { Maximize2, Minimize2 } from "react-feather"; +import { v4 } from "uuid"; +import { decode as HTMLDecode } from "he"; + +function combineLikeSources(sources) { + const combined = {}; + sources.forEach((source) => { + const { id, title, text } = source; + if (combined.hasOwnProperty(title)) { + combined[title].text += `\n\n ---- Chunk ${id || ""} ---- \n\n${text}`; + combined[title].references += 1; + } else { + combined[title] = { title, text, references: 1 }; + } + }); + return Object.values(combined); +} + +export default function Citations({ sources = [] }) { + if (sources.length === 0) return null; + + return ( + <div className="flex flex-col mt-4 justify-left"> + <div className="no-scroll flex flex-col justify-left overflow-x-scroll "> + <div className="w-full flex overflow-x-scroll items-center gap-4 mt-1 doc__source"> + {combineLikeSources(sources).map((source) => ( + <Citation id={source?.id || v4()} source={source} /> + ))} + </div> + </div> + <p className="w-fit text-gray-700 dark:text-stone-400 text-xs mt-1"> + *citations may not be relevant to end result. + </p> + </div> + ); +} + +const Citation = memo(({ source, id }) => { + const [maximized, setMaximized] = useState(false); + const { references = 0, title, text } = source; + if (title?.length === 0 || text?.length === 0) return null; + const handleMinMax = () => { + setMaximized(!maximized); + Array.from( + document?.querySelectorAll( + `div[data-citation]:not([data-citation="${id}"])` + ) + ).forEach((el) => { + const func = maximized ? "remove" : "add"; + el.classList[func]("hidden"); + }); + }; + + return ( + <div + key={id || v4()} + data-citation={id || v4()} + className={`transition-all duration-300 relative flex flex-col w-full md:w-80 h-40 bg-gray-100 dark:bg-stone-800 border border-gray-700 dark:border-stone-800 rounded-lg shrink-0 ${ + maximized ? "md:w-full h-fit pb-4" : "" + }`} + > + <div className="rounded-t-lg bg-gray-300 dark:bg-stone-900 px-4 py-2 w-full h-fit flex items-center justify-between"> + <p className="text-base text-gray-800 dark:text-slate-400 italic truncate w-3/4"> + {title} + </p> + <button + onClick={handleMinMax} + className="hover:dark:bg-stone-800 hover:bg-gray-200 dark:text-slate-400 text-gray-800 rounded-full p-1" + > + {maximized ? ( + <Minimize2 className="h-4 w-4" /> + ) : ( + <Maximize2 className="h-4 w-4" /> + )} + </button> + </div> + <div + className={`overflow-hidden relative w-full ${ + maximized ? "overflow-y-scroll" : "" + }`} + > + <p className="px-2 py-1 text-xs whitespace-pre-line text-gray-800 dark:text-slate-300 italic"> + {references > 1 && ( + <p className="text-xs text-gray-500 dark:text-slate-500 mb-2"> + referenced {references} times. + </p> + )} + {HTMLDecode(text)} + </p> + <div + className={`absolute bottom-0 flex w-full h-[20px] fade-up-border rounded-b-lg ${ + maximized ? "hidden" : "" + }`} + /> + </div> + </div> + ); +}); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index ee5af67eca016d0bf7c32c22f42a632dd04f287d..4acda9b8a99e74f3e66184e891d0284462535e65 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -1,10 +1,9 @@ -import { useEffect, useRef, memo, useState } from "react"; +import { useEffect, useRef, memo } from "react"; import { AlertTriangle } from "react-feather"; import Jazzicon from "../../../../UserIcon"; -import { v4 } from "uuid"; -import { decode as HTMLDecode } from "he"; import renderMarkdown from "../../../../../utils/chat/markdown"; import { userFromStorage } from "../../../../../utils/request"; +import Citations from "../Citation"; function HistoricalMessage({ message, @@ -55,7 +54,7 @@ function HistoricalMessage({ <Jazzicon size={30} user={{ uid: workspace.slug }} /> <div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm"> <span - className="whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base flex flex-col gap-y-1" + className="no-scroll whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base flex flex-col gap-y-1" dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }} /> <Citations sources={sources} /> @@ -64,46 +63,4 @@ function HistoricalMessage({ ); } -const Citations = ({ sources = [] }) => { - const [show, setShow] = useState(false); - if (sources.length === 0) return null; - - return ( - <div className="flex flex-col mt-4 justify-left"> - <button - type="button" - onClick={() => setShow(!show)} - className="w-fit text-gray-700 dark:text-stone-400 italic text-xs" - > - {show ? "hide" : "show"} citations{show && "*"} - </button> - {show && ( - <> - <div className="w-full flex flex-wrap items-center gap-4 mt-1 doc__source"> - {sources.map((source) => { - const { id = null, title, url } = source; - const handleClick = () => { - if (!url) return false; - window.open(url, "_blank"); - }; - return ( - <button - key={id || v4()} - onClick={handleClick} - className="italic transition-all duration-300 w-fit bg-gray-400 text-gray-900 py-[1px] hover:text-slate-200 hover:bg-gray-500 hover:dark:text-gray-900 dark:bg-stone-400 dark:hover:bg-stone-300 rounded-full px-2 text-xs leading-tight" - > - "{HTMLDecode(title)}" - </button> - ); - })} - </div> - <p className="w-fit text-gray-700 dark:text-stone-400 text-xs mt-1"> - *citation may not be relevant to end result. - </p> - </> - )} - </div> - ); -}; - export default memo(HistoricalMessage); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx index a0bf02bc3d853f604620c7a203ca14135949de8d..a82c25a4815731efabc14d5527a42f806b1505d8 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx @@ -1,9 +1,8 @@ -import { memo, useEffect, useRef, useState } from "react"; +import { memo, useEffect, useRef } from "react"; import { AlertTriangle } from "react-feather"; import Jazzicon from "../../../../UserIcon"; -import { v4 } from "uuid"; -import { decode as HTMLDecode } from "he"; import renderMarkdown from "../../../../../utils/chat/markdown"; +import Citations from "../Citation"; function PromptReply({ uuid, @@ -69,46 +68,4 @@ function PromptReply({ ); } -const Citations = ({ sources = [] }) => { - const [show, setShow] = useState(false); - if (sources.length === 0) return null; - - return ( - <div className="flex flex-col mt-4 justify-left"> - <button - type="button" - onClick={() => setShow(!show)} - className="w-fit text-gray-700 dark:text-stone-400 italic text-xs" - > - {show ? "hide" : "show"} citations{show && "*"} - </button> - {show && ( - <> - <div className="w-full flex flex-wrap items-center gap-4 mt-1 doc__source"> - {sources.map((source) => { - const { id = null, title, url } = source; - const handleClick = () => { - if (!url) return false; - window.open(url, "_blank"); - }; - return ( - <button - key={id || v4()} - onClick={handleClick} - className="italic transition-all duration-300 w-fit bg-gray-400 text-gray-900 py-[1px] hover:text-slate-200 hover:bg-gray-500 hover:dark:text-gray-900 dark:bg-stone-400 dark:hover:bg-stone-300 rounded-full px-2 text-xs leading-tight" - > - "{HTMLDecode(title)}" - </button> - ); - })} - </div> - <p className="w-fit text-gray-700 dark:text-stone-400 text-xs mt-1"> - *citation may not be relevant to end result. - </p> - </> - )} - </div> - ); -}; - export default memo(PromptReply); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx index 0ecf3238f1857df315810633f8b0c53ddcbc13ee..b0e1e7a89a97c51be6af3a8135db2ad3bf7a2058 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx @@ -65,7 +65,7 @@ export default function PromptInput({ <button onClick={() => setShowMenu(!showMenu)} type="button" - className="p-2 text-slate-200 bg-transparent rounded-md hover:bg-gray-50 dark:hover:bg-stone-500" + className="p-2 text-slate-500 bg-transparent rounded-md hover:bg-gray-200 dark:hover:bg-stone-500 dark:hover:text-slate-200" > <Menu className="w-4 h-4 md:h-6 md:w-6" /> </button> @@ -93,14 +93,14 @@ export default function PromptInput({ ref={formRef} type="submit" disabled={buttonDisabled} - className="inline-flex justify-center p-0 md:p-2 rounded-full cursor-pointer text-black-900 dark:text-slate-200 hover:bg-gray-600 dark:hover:bg-stone-500" + className="inline-flex justify-center p-0 md:p-2 rounded-full cursor-pointer text-black-900 dark:text-slate-200 hover:bg-gray-200 dark:hover:bg-stone-500 group" > {buttonDisabled ? ( <Loader className="w-6 h-6 animate-spin" /> ) : ( <svg aria-hidden="true" - className="w-6 h-6 rotate-45" + className="w-6 h-6 rotate-45 fill-gray-500 dark:fill-slate-500 group-hover:dark:fill-slate-200" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" @@ -138,7 +138,7 @@ const Tracking = memo(({ workspaceSlug }) => { return ( <div className="flex flex-col md:flex-row w-full justify-center items-center gap-2 mb-2 px-4 mx:px-0"> - <p className="bg-stone-600 text-slate-400 text-xs px-2 rounded-lg font-mono text-center"> + <p className="bg-gray-200 dark:bg-stone-600 text-gray-800 dark:text-slate-400 text-xs px-2 rounded-lg font-mono text-center"> Chat mode: {chatMode} </p> <p className="text-slate-400 text-xs text-center"> @@ -164,13 +164,13 @@ function CommandMenu({ workspace, show, handleClick, hide }) { ]; return ( - <div className="absolute top-[-25vh] md:top-[-23vh] min-h-[200px] flex flex-col rounded-lg border border-slate-400 p-2 pt-4 bg-stone-600"> + <div className="absolute top-[-25vh] md:top-[-23vh] min-h-[200px] flex flex-col rounded-lg border border-slate-400 p-2 pt-4 bg-gray-50 dark:bg-stone-600"> <div className="flex justify-between items-center border-b border-slate-400 px-2 py-1 "> - <p className="text-slate-200">Available Commands</p> + <p className="text-gray-800 dark:text-slate-200">Available Commands</p> <button type="button" onClick={hide} - className="p-2 rounded-lg hover:bg-slate-500 rounded-full text-slate-400" + className="p-2 rounded-lg hover:bg-gray-200 hover:dark:bg-slate-500 rounded-full text-gray-800 dark:text-slate-400" > <X className="h-4 w-4" /> </button> @@ -188,10 +188,14 @@ function CommandMenu({ workspace, show, handleClick, hide }) { handleClick(cmd); hide(); }} - className="w-full px-4 py-2 flex items-center rounded-lg hover:bg-slate-500 gap-x-1 disabled:cursor-not-allowed" + className="w-full px-4 py-2 flex items-center rounded-lg hover:bg-gray-300 hover:dark:bg-slate-500 gap-x-1 disabled:cursor-not-allowed" > - <p className="text-slate-200 font-semibold">{cmd}</p> - <p className="text-slate-400 text-sm">{description}</p> + <p className="text-gray-800 dark:text-slate-200 font-semibold"> + {cmd} + </p> + <p className="text-gray-800 dark:text-slate-300 text-sm"> + {description} + </p> </button> </div> ); diff --git a/frontend/src/index.css b/frontend/src/index.css index 225a8d4572437c42991d3dcee076fbd711ef0640..a0294126fe7aa714514605ad8122539ce35c338c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -133,6 +133,26 @@ a { } } +@media (prefers-color-scheme: light) { + .fade-up-border { + background: linear-gradient( + to bottom, + rgba(220, 221, 223, 10%), + rgb(220, 221, 223) 89% + ); + } +} + +@media (prefers-color-scheme: dark) { + .fade-up-border { + background: linear-gradient( + to bottom, + rgba(41, 37, 36, 50%), + rgb(41 37 36) 90% + ); + } +} + /** * ============================================== * Dot Falling diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index cad2efe7f894fb15209c34f0d308f1c248a08f99..e0d23bb3f1bb1928656598c2a70f21dfa54de30a 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -5,7 +5,7 @@ export default { extend: { colors: { 'black-900': '#141414', - } + }, }, }, plugins: [], diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 745fd06153df18778d7fb960a96e56991b370acc..672bddea945822b4daa2ba4fa400cd454bcae8d5 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -23,7 +23,6 @@ function toChunks(arr, size) { } function curateSources(sources = []) { - const knownDocs = []; const documents = []; // Sometimes the source may or may not have a metadata property @@ -32,17 +31,12 @@ function curateSources(sources = []) { for (const source of sources) { if (source.hasOwnProperty("metadata")) { const { metadata = {} } = source; - if ( - Object.keys(metadata).length > 0 && - !knownDocs.includes(metadata.title) - ) { + if (Object.keys(metadata).length > 0) { documents.push({ ...metadata }); - knownDocs.push(metadata.title); } } else { - if (Object.keys(source).length > 0 && !knownDocs.includes(source.title)) { + if (Object.keys(source).length > 0) { documents.push({ ...source }); - knownDocs.push(source.title); } } } diff --git a/server/utils/vectorDbProviders/lance/index.js b/server/utils/vectorDbProviders/lance/index.js index ddc184693c7acd820acda0920f3e2afaaa9c7ac8..bf29d17f6ef4eb9cff663ebca51c675ece12ee7e 100644 --- a/server/utils/vectorDbProviders/lance/index.js +++ b/server/utils/vectorDbProviders/lance/index.js @@ -10,16 +10,11 @@ const { chatPrompt } = require("../../chats"); // Since we roll our own results for prompting we // have to manually curate sources as well. function curateLanceSources(sources = []) { - const knownDocs = []; const documents = []; for (const source of sources) { - const { text: _t, vector: _v, score: _s, ...metadata } = source; - if ( - Object.keys(metadata).length > 0 && - !knownDocs.includes(metadata.title) - ) { - documents.push({ ...metadata }); - knownDocs.push(metadata.title); + const { text, vector: _v, score: _s, ...metadata } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ ...metadata, text }); } }