diff --git a/frontend/src/components/Sidebar/SidebarToggle/index.jsx b/frontend/src/components/Sidebar/SidebarToggle/index.jsx index 30aac82c..230adbd8 100644 --- a/frontend/src/components/Sidebar/SidebarToggle/index.jsx +++ b/frontend/src/components/Sidebar/SidebarToggle/index.jsx @@ -3,6 +3,7 @@ import { SidebarSimple } from "@phosphor-icons/react"; import paths from "@/utils/paths"; import { Tooltip } from "react-tooltip"; const SIDEBAR_TOGGLE_STORAGE_KEY = "anythingllm_sidebar_toggle"; +export const SIDEBAR_TOGGLE_EVENT = "sidebar-toggle"; /** * Returns the previous state of the sidebar from localStorage. @@ -62,6 +63,11 @@ export function useSidebarToggle() { SIDEBAR_TOGGLE_STORAGE_KEY, showSidebar ? "open" : "closed" ); + window.dispatchEvent( + new CustomEvent(SIDEBAR_TOGGLE_EVENT, { + detail: { open: showSidebar }, + }) + ); }, [showSidebar]); return { showSidebar, setShowSidebar, canToggleSidebar }; diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx index f47c5852..aab732c9 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx @@ -31,7 +31,6 @@ import CustomCell from "./CustomCell.jsx"; import Tooltip from "./CustomTooltip.jsx"; import { safeJsonParse } from "@/utils/request.js"; import renderMarkdown from "@/utils/chat/markdown.js"; -import { WorkspaceProfileImage } from "../PromptReply/index.jsx"; import { memo, useCallback, useState } from "react"; import { saveAs } from "file-saver"; import { useGenerateImage } from "recharts-to-png"; @@ -41,7 +40,7 @@ const dataFormatter = (number) => { return Intl.NumberFormat("us").format(number).toString(); }; -export function Chartable({ props, workspace }) { +export function Chartable({ props }) { const [getDivJpeg, { ref }] = useGenerateImage({ quality: 1, type: "image/jpeg", @@ -387,20 +386,17 @@ export function Chartable({ props, workspace }) { if (!!props.chatId) { return ( -
-
-
- -
- -
{renderChart()}
- -
+
+
+
+ +
{renderChart()}
+
@@ -408,20 +404,18 @@ export function Chartable({ props, workspace }) { } return ( -
-
+
+
{renderChart()}
-
- -
+
); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx index eee2a395..c588b4ec 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -1,10 +1,8 @@ -import { Fragment, useState } from "react"; +import { Fragment } from "react"; import { decode as HTMLDecode } from "he"; import truncate from "truncate"; import ModalWrapper from "@/components/ModalWrapper"; -import { middleTruncate } from "@/utils/directories"; import { - CaretRight, FileText, Info, ArrowSquareOut, @@ -14,16 +12,41 @@ import { LinkSimple, GitlabLogo, } from "@phosphor-icons/react"; -import ConfluenceLogo from "@/media/dataConnectors/confluence.png"; -import DrupalWikiLogo from "@/media/dataConnectors/drupalwiki.png"; -import ObsidianLogo from "@/media/dataConnectors/obsidian.png"; -import PaperlessNgxLogo from "@/media/dataConnectors/paperlessngx.png"; import { toPercentString } from "@/utils/numbers"; import { useTranslation } from "react-i18next"; -import pluralize from "pluralize"; -import useTextSize from "@/hooks/useTextSize"; +import { useSourcesSidebar } from "../../SourcesSidebar"; -function combineLikeSources(sources) { +const CIRCLE_ICONS = { + file: FileText, + link: LinkSimple, + youtube: YoutubeLogo, + github: GithubLogo, + gitlab: GitlabLogo, + confluence: LinkSimple, + drupalwiki: FileText, + obsidian: FileText, + paperlessNgx: FileText, +}; + +/** + * Renders a circle with a source type icon inside. + * @param {"file"|"link"|"youtube"|"github"|"gitlab"|"confluence"|"drupalwiki"|"obsidian"|"paperlessNgx"} props.type + * @param {number} [props.size] - Circle diameter in px + * @param {number} [props.iconSize] - Icon size in px + */ +export function SourceTypeCircle({ type = "file", size = 22, iconSize = 12 }) { + const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file; + return ( +
+ +
+ ); +} + +export function combineLikeSources(sources) { const combined = {}; sources.forEach((source) => { const { id, title, text, chunkSource = "", score = null } = source; @@ -42,106 +65,83 @@ function combineLikeSources(sources) { } export default function Citations({ sources = [] }) { - const [open, setOpen] = useState(false); - const [selectedSource, setSelectedSource] = useState(null); + const { + sidebarOpen, + openSidebar, + closeSidebar, + sources: currentSources, + } = useSourcesSidebar(); const { t } = useTranslation(); - const { textSizeClass } = useTextSize(); - if (sources.length === 0) return null; - return ( -
- - {open && ( -
- {combineLikeSources(sources).map((source, idx) => ( - setSelectedSource(source)} - textSizeClass={textSizeClass} - /> - ))} -
- )} - {selectedSource && ( - setSelectedSource(null)} - /> - )} -
- ); -} + const combined = combineLikeSources(sources); + const visibleSources = combined.slice(0, 3); + const remainingCount = Math.max(0, combined.length - 3); -const Citation = ({ source, onClick, textSizeClass }) => { - const { title, references = 1 } = source; - if (!title) return null; - const chunkSourceInfo = parseChunkSource(source); - const truncatedTitle = chunkSourceInfo?.text ?? middleTruncate(title, 25); - const CitationIcon = ICONS.hasOwnProperty(chunkSourceInfo?.icon) - ? ICONS[chunkSourceInfo.icon] - : ICONS.file; + function handleOpenSourcesSidebar() { + if (sidebarOpen && sources === currentSources) { + closeSidebar(); + } else { + openSidebar(sources); + } + } return ( ); -}; +} -function omitChunkHeader(text) { +export function omitChunkHeader(text) { if (!text.includes("")) return text; return text.split("")[1].trim(); } -function CitationDetailModal({ source, onClose }) { +export function CitationDetailModal({ source, onClose }) { const { references, title, chunks } = source; const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source); + const { t } = useTranslation(); return ( -
-
+
+
{references > 1 && ( -

+

Referenced {references} times.

)}
{chunks.map(({ text, score }, idx) => ( -
+
-

+

{HTMLDecode(omitChunkHeader(text))}

{!!score && ( -
+
-

{toPercentString(score)} match

+

+ {toPercentString(score)}{" "} + {t("chat_window.similarity_match")} +

)}
{idx !== chunks.length - 1 && ( -
+
)} ))} @@ -228,7 +235,7 @@ const supportedSources = [ * @param {{title: string, chunks: {text: string, chunkSource: string}[]}} options * @returns {{isUrl: boolean, text: string, href: string, icon: string}} */ -function parseChunkSource({ title = "", chunks = [] }) { +export function parseChunkSource({ title = "", chunks = [] }) { const nullResponse = { isUrl: false, text: null, @@ -315,33 +322,3 @@ function parseChunkSource({ title = "", chunks = [] }) { } return nullResponse; } - -const ConfluenceIcon = ({ size = 16, ...props }) => ( - -); -const DrupalWikiIcon = ({ size = 16, ...props }) => ( - -); -const ObsidianIcon = ({ size = 16, ...props }) => ( - -); -const PaperlessNgxIcon = ({ size = 16, ...props }) => ( - -); -const ICONS = { - file: FileText, - link: LinkSimple, - youtube: YoutubeLogo, - github: GithubLogo, - gitlab: GitlabLogo, - confluence: ConfluenceIcon, - drupalwiki: DrupalWikiIcon, - obsidian: ObsidianIcon, - paperlessNgx: PaperlessNgxIcon, -}; diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx index 829c1b41..f77af8bd 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx @@ -40,7 +40,7 @@ function ActionMenu({ chatId, forkThread, isEditing, role }) {
); @@ -75,17 +71,30 @@ export function EditMessageForm({ saveChanges, }) { const formRef = useRef(null); - const { t } = useTranslation(); - function handleSaveMessage(e) { + + function handleSubmit(e) { e.preventDefault(); - const form = new FormData(e.target); - const editedMessage = form.get("editedMessage"); + const editedMessage = formRef.current.value; saveChanges({ editedMessage, chatId, role, attachments }); window.dispatchEvent( new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } }) ); } + function handleSave() { + const editedMessage = formRef.current.value; + saveChanges({ + editedMessage, + chatId, + role, + attachments, + saveOnly: true, + }); + window.dispatchEvent( + new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } }) + ); + } + function cancelEdits() { window.dispatchEvent( new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } }) @@ -94,36 +103,91 @@ export function EditMessageForm({ } useEffect(() => { - if (!formRef || !formRef.current) return; + if (!formRef?.current) return; formRef.current.focus(); adjustTextArea({ target: formRef.current }); - }, [formRef]); + }, []); + + if (role === "user") { + return ( +
+