From 1695f19c8802c7d26e0aacd3793595f8ada5c884 Mon Sep 17 00:00:00 2001 From: Prakriti Solankey <156313631+prakriti-solankey@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:41:07 +0530 Subject: [PATCH] Graph communities (#748) * UI changes * modes enable disable * separated sources entities chunk communities * communities added into separate component * Update ChatInfoModal.tsx * added filename and source for chunksinfo * removed the console.log * mode disable changes --------- Co-authored-by: kartikpersistent <101251502+kartikpersistent@users.noreply.github.com> --- backend/src/chunkid_entities.py | 3 + frontend/src/App.css | 2 +- .../src/components/ChatBot/ChatInfoModal.tsx | 413 ++---------------- .../src/components/ChatBot/ChatModeToggle.tsx | 64 +-- frontend/src/components/ChatBot/ChunkInfo.tsx | 127 ++++++ .../src/components/ChatBot/Communities.tsx | 36 ++ .../src/components/ChatBot/EntitiesInfo.tsx | 91 ++++ .../src/components/ChatBot/SourcesInfo.tsx | 139 ++++++ frontend/src/types.ts | 24 + frontend/src/utils/Utils.ts | 11 + 10 files changed, 508 insertions(+), 402 deletions(-) create mode 100644 frontend/src/components/ChatBot/ChunkInfo.tsx create mode 100644 frontend/src/components/ChatBot/Communities.tsx create mode 100644 frontend/src/components/ChatBot/EntitiesInfo.tsx create mode 100644 frontend/src/components/ChatBot/SourcesInfo.tsx diff --git a/backend/src/chunkid_entities.py b/backend/src/chunkid_entities.py index 53a0544f6..3fd3c3a71 100644 --- a/backend/src/chunkid_entities.py +++ b/backend/src/chunkid_entities.py @@ -135,6 +135,9 @@ def process_entityids(driver, chunk_ids): logging.info(f"Nodes and relationships are processed") result["chunk_data"] = records[0]["chunks"] result["community_data"] = records[0]["communities"] + else: + result["chunk_data"] = list() + result["community_data"] = list() logging.info(f"Query process completed successfully for chunk ids: {chunk_ids}") return result except Exception as e: diff --git a/frontend/src/App.css b/frontend/src/App.css index 615b8559b..e912a05e2 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -385,5 +385,5 @@ .custom-menu { min-width: 250px; - max-width: 300px; + max-width: 305px; } \ No newline at end of file diff --git a/frontend/src/components/ChatBot/ChatInfoModal.tsx b/frontend/src/components/ChatBot/ChatInfoModal.tsx index 192b37407..82fa25e26 100644 --- a/frontend/src/components/ChatBot/ChatInfoModal.tsx +++ b/frontend/src/components/ChatBot/ChatInfoModal.tsx @@ -1,48 +1,36 @@ import { Box, Typography, - TextLink, Flex, Tabs, - LoadingSpinner, CypherCodeBlock, CypherCodeBlockProps, useCopyToClipboard, Banner, useMediaQuery, } from '@neo4j-ndl/react'; -import { - DocumentDuplicateIconOutline, - DocumentTextIconOutline, - ClipboardDocumentCheckIconOutline, - GlobeAltIconOutline, -} from '@neo4j-ndl/react/icons'; +import { DocumentDuplicateIconOutline, ClipboardDocumentCheckIconOutline } from '@neo4j-ndl/react/icons'; import '../../styling/info.css'; import Neo4jRetrievalLogo from '../../assets/images/Neo4jRetrievalLogo.png'; -import wikipedialogo from '../../assets/images/wikipedia.svg'; -import youtubelogo from '../../assets/images/youtube.svg'; -import gcslogo from '../../assets/images/gcs.webp'; -import s3logo from '../../assets/images/s3logo.png'; import { Chunk, Community, Entity, ExtendedNode, ExtendedRelationship, - GroupedEntity, UserCredentials, chatInfoMessage, } from '../../types'; import { useContext, useEffect, useMemo, useState } from 'react'; -import HoverableLink from '../UI/HoverableLink'; import GraphViewButton from '../Graph/GraphViewButton'; import { chunkEntitiesAPI } from '../../services/ChunkEntitiesInfo'; import { useCredentials } from '../../context/UserCredentials'; -import { calcWordColor } from '@neo4j-devtools/word-color'; -import ReactMarkdown from 'react-markdown'; -import { getLogo, parseEntity, youtubeLinkValidation } from '../../utils/Utils'; import { ThemeWrapperContext } from '../../context/ThemeWrapper'; import { tokens } from '@neo4j-ndl/base'; +import ChunkInfo from './ChunkInfo'; +import EntitiesInfo from './EntitiesInfo'; +import SourcesInfo from './SourcesInfo'; +import CommunitiesInfo from './Communities'; const ChatInfoModal: React.FC = ({ sources, @@ -106,8 +94,13 @@ const ChatInfoModal: React.FC = ({ if (response.data.status === 'Failure') { throw new Error(response.data.error); } + const nodesData = response?.data?.data?.nodes; + const relationshipsData = response?.data?.data?.relationships; + const communitiesData = response?.data?.data?.community_data; + const chunksData = response?.data?.data?.chunk_data; + setInfoEntities( - response.data.data.nodes.map((n: Entity) => { + nodesData.map((n: Entity) => { if (!n.labels.length && mode === 'entity search+vector') { return { ...n, @@ -118,30 +111,34 @@ const ChatInfoModal: React.FC = ({ }) ); setNodes( - response.data.data.nodes.map((n: ExtendedNode) => { + nodesData.map((n: ExtendedNode) => { if (!n.labels.length && mode === 'entity search+vector') { return { ...n, labels: ['Entity'], }; } - return n; + return n ?? []; }) ); - setRelationships(response.data.data.relationships); - setCommunities(response.data.data.community_data); - const chunks = response.data.data.chunk_data.map((chunk: any) => { - const chunkScore = chunk_ids.find((chunkdetail) => chunkdetail.id === chunk.id); - return { - ...chunk, - score: chunkScore?.score, - }; - }); - const sortedchunks = chunks.sort((a: any, b: any) => b.score - a.score); - setChunks(sortedchunks); + setRelationships(relationshipsData ?? []); + setCommunities(communitiesData ?? []); + setChunks( + chunksData + .map((chunk: any) => { + const chunkScore = chunk_ids.find((chunkdetail) => chunkdetail.id === chunk.id); + return ( + { + ...chunk, + score: chunkScore?.score, + } ?? [] + ); + }) + .sort((a: any, b: any) => b.score - a.score) + ); setLoading(false); } catch (error) { - console.error('Error fetching entities:', error); + console.error('Error fetching information:', error); setLoading(false); } })(); @@ -151,50 +148,10 @@ const ChatInfoModal: React.FC = ({ }; }, [chunk_ids, mode, error]); - const groupedEntities = useMemo<{ [key: string]: GroupedEntity }>(() => { - const items = infoEntities.reduce((acc, entity) => { - const { label, text } = parseEntity(entity); - if (!acc[label]) { - console.log({ label, text }); - const newColor = calcWordColor(label); - acc[label] = { texts: new Set(), color: newColor }; - } - acc[label].texts.add(text); - return acc; - }, {} as Record; color: string }>); - return items; - }, [infoEntities]); - const onChangeTabs = (tabId: number) => { setActiveTab(tabId); }; - const labelCounts = useMemo(() => { - const counts: { [label: string]: number } = {}; - for (let index = 0; index < infoEntities?.length; index++) { - const entity = infoEntities[index]; - const { labels } = entity; - const [label] = labels; - counts[label] = counts[label] ? counts[label] + 1 : 1; - } - return counts; - }, [infoEntities]); - - const sortedLabels = useMemo(() => { - return Object.keys(labelCounts).sort((a, b) => labelCounts[b] - labelCounts[a]); - }, [labelCounts]); - - const generateYouTubeLink = (url: string, startTime: string) => { - try { - const urlObj = new URL(url); - urlObj.searchParams.set('t', startTime); - return urlObj.toString(); - } catch (error) { - console.error('Invalid URL:', error); - return ''; - } - }; - return ( @@ -237,290 +194,18 @@ const ChatInfoModal: React.FC = ({ )} - {loading ? ( - - - - ) : mode === 'entity search+vector' && chunks?.length ? ( -
    - {chunks - .map((c) => ({ fileName: c.fileName, fileSource: c.fileSource })) - .map((s, index) => { - return ( -
  • -
    - {s.fileSource === 'local file' ? ( - - ) : ( - - )} - - {s.fileName} - -
    -
  • - ); - })} -
- ) : sources?.length ? ( -
    - {sources.map((link, index) => { - return ( -
  • - {link?.startsWith('http') || link?.startsWith('https') ? ( - <> - {link?.includes('wikipedia.org') && ( -
    - Wikipedia Logo - - - - {link} - - - -
    - )} - {link?.includes('storage.googleapis.com') && ( -
    - Google Cloud Storage Logo - - {decodeURIComponent(link).split('/').at(-1)?.split('?')[0] ?? 'GCS File'} - -
    - )} - {youtubeLinkValidation(link) && ( - <> -
    - - - - - {link} - - - -
    - - )} - {!link?.startsWith('s3://') && - !link?.includes('storage.googleapis.com') && - !link?.includes('wikipedia.org') && - !link?.includes('youtube.com') && ( -
    - - - {link} - -
    - )} - - ) : link?.startsWith('s3://') ? ( -
    - S3 Logo - - {decodeURIComponent(link).split('/').at(-1) ?? 'S3 File'} - -
    - ) : ( -
    - - - {link} - -
    - )} -
  • - ); - })} -
- ) : ( - No Sources Found - )} +
- {loading ? ( - - - - ) : Object.keys(groupedEntities)?.length > 0 || Object.keys(graphonly_entities)?.length > 0 ? ( -
    - {mode == 'graph' - ? graphonly_entities.map((label, index) => ( -
  • -
    - { - // @ts-ignore - label[Object.keys(label)[0]].id ?? Object.keys(label)[0] - } -
    -
  • - )) - : sortedLabels.map((label, index) => { - const entity = groupedEntities[label == 'undefined' ? 'Entity' : label]; - return ( -
  • -
    - {label} ({labelCounts[label]}) -
    - - {Array.from(entity.texts).slice(0, 3).join(', ')} - -
  • - ); - })} -
- ) : ( - No Entities Found - )} +
- {loading ? ( - - - - ) : chunks?.length > 0 ? ( -
-
    - {chunks.map((chunk) => ( -
  • - {chunk?.page_number ? ( - <> -
    - - - {/* {chunk?.fileName}, Page: {chunk?.page_number} */} - {chunk?.fileName} - -
    - Similarity Score: {chunk?.score} - - ) : chunk?.url && chunk?.start_time ? ( - <> -
    - - - - {chunk?.fileName} - - -
    - Similarity Score: {chunk?.score} - - ) : chunk?.url && chunk?.url.includes('wikipedia.org') ? ( - <> -
    - - {chunk?.fileName} -
    - Similarity Score: {chunk?.score} - - ) : chunk?.url && chunk?.url.includes('storage.googleapis.com') ? ( - <> -
    - - {chunk?.fileName} -
    - Similarity Score: {chunk?.score} - - ) : chunk?.url && chunk?.url.startsWith('s3://') ? ( - <> -
    - - {chunk?.fileName} -
    - Similarity Score: {chunk?.score} - - ) : chunk?.url && - !chunk?.url.startsWith('s3://') && - !chunk?.url.includes('storage.googleapis.com') && - !chunk?.url.includes('wikipedia.org') && - !chunk?.url.includes('youtube.com') ? ( - <> -
    - - - {chunk?.url} - -
    - Similarity Score: {chunk?.score} - - ) : ( - <> -
    - {chunk.fileSource === 'local file' ? ( - - ) : ( - - )} - - {chunk.fileName} - -
    - - )} - - Text: {chunk?.text} - -
  • - ))} -
-
- ) : ( - No Chunks Found - )} +
= ({ /> {mode === 'entity search+vector' ? ( - - {loading ? ( - - - - ) : ( -
-
    - {communities.map((community, index) => ( -
  • -
    - - ID : - {community.id} - - {community.summary} -
    -
  • - ))} -
-
- )} + + ) : ( <> diff --git a/frontend/src/components/ChatBot/ChatModeToggle.tsx b/frontend/src/components/ChatBot/ChatModeToggle.tsx index a0ebe879a..293706910 100644 --- a/frontend/src/components/ChatBot/ChatModeToggle.tsx +++ b/frontend/src/components/ChatBot/ChatModeToggle.tsx @@ -8,7 +8,7 @@ import { capitalizeWithPlus } from '../../utils/Utils'; import { useCredentials } from '../../context/UserCredentials'; export default function ChatModeToggle({ menuAnchor, - closeHandler = () => {}, + closeHandler = () => { }, open, anchorPortal = true, disableBackdrop = false, @@ -19,7 +19,7 @@ export default function ChatModeToggle({ anchorPortal?: boolean; disableBackdrop?: boolean; }) { - const { setchatMode, chatMode, postProcessingTasks } = useFileContext(); + const { setchatMode, chatMode, postProcessingTasks, selectedRows } = useFileContext(); const isCommunityAllowed = postProcessingTasks.includes('create_communities'); const { isGdsActive } = useCredentials(); const memoizedChatModes = useMemo(() => { @@ -27,34 +27,44 @@ export default function ChatModeToggle({ ? chatModes : chatModes?.filter((m) => !m.mode.includes('entity search+vector')); }, [isGdsActive, isCommunityAllowed]); + + const menuItems = useMemo(() => { - return memoizedChatModes?.map((m) => ({ - title: ( -
- - {m.mode.includes('+') ? capitalizeWithPlus(m.mode) : capitalize(m.mode)} - + return memoizedChatModes?.map((m) => { + const isDisabled = Boolean(selectedRows.length && !(m.mode === 'vector' || m.mode === 'graph+vector')); + return { + title: (
- {m.description} + + {m.mode.includes('+') ? capitalizeWithPlus(m.mode) : capitalize(m.mode)} + +
+ {m.description} +
-
- ), - onClick: () => { - setchatMode(m.mode); - closeHandler(); // Close the menu after setting the chat mode - }, - disabledCondition: false, - description: ( - - {chatMode === m.mode && ( - <> - Selected - - )} - - ), - })); - }, [chatMode, memoizedChatModes, setchatMode, closeHandler]); + ), + onClick: () => { + setchatMode(m.mode); + closeHandler(); + }, + disabledCondition: isDisabled, + description: ( + + {chatMode === m.mode && ( + <> + Selected + + )} + {isDisabled && ( + <> + Chatmode not available + + )} + + ), + }; + }); + }, [chatMode, memoizedChatModes, setchatMode, closeHandler, selectedRows]); return ( = ({ loading, chunks }) => { + const themeUtils = useContext(ThemeWrapperContext); + + return ( + <> + {loading ? ( + + + + ) : chunks?.length > 0 ? ( +
+
    + {chunks.map((chunk) => ( +
  • + {chunk?.page_number ? ( + <> +
    + + + {chunk?.fileName} + +
    + Similarity Score: {chunk?.score} + + ) : chunk?.url && chunk?.start_time ? ( + <> +
    + + + + {chunk?.fileName} + + +
    + Similarity Score: {chunk?.score} + + ) : chunk?.url && chunk?.url.includes('wikipedia.org') ? ( + <> +
    + + {chunk?.fileName} +
    + Similarity Score: {chunk?.score} + + ) : chunk?.url && chunk?.url.includes('storage.googleapis.com') ? ( + <> +
    + + {chunk?.fileName} +
    + Similarity Score: {chunk?.score} + + ) : chunk?.url && chunk?.url.startsWith('s3://') ? ( + <> +
    + + {chunk?.fileName} +
    + Similarity Score: {chunk?.score} + + ) : chunk?.url && + !chunk?.url.startsWith('s3://') && + !chunk?.url.includes('storage.googleapis.com') && + !chunk?.url.includes('wikipedia.org') && + !chunk?.url.includes('youtube.com') ? ( + <> +
    + + + {chunk?.url} + +
    + Similarity Score: {chunk?.score} + + ) : ( + <> +
    + {chunk.fileSource === 'local file' ? ( + + ) : ( + + )} + + {chunk.fileName} + +
    + + )} + {chunk?.text} +
  • + ))} +
+
+ ) : ( + No Chunks Found + )} + + ); +}; + +export default ChunkInfo; diff --git a/frontend/src/components/ChatBot/Communities.tsx b/frontend/src/components/ChatBot/Communities.tsx new file mode 100644 index 000000000..9b530fd0f --- /dev/null +++ b/frontend/src/components/ChatBot/Communities.tsx @@ -0,0 +1,36 @@ +import { Box, LoadingSpinner, Flex, Typography } from '@neo4j-ndl/react'; +import { FC } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { CommunitiesProps } from '../../types'; + +const CommunitiesInfo: FC = ({ loading, communities }) => { + return ( + <> + {loading ? ( + + + + ) : communities?.length > 0 ? ( +
+
    + {communities.map((community, index) => ( +
  • +
    + + ID : + {community.id} + + {community.summary} +
    +
  • + ))} +
+
+ ) : ( + No Communities Found + )} + + ); +}; + +export default CommunitiesInfo; diff --git a/frontend/src/components/ChatBot/EntitiesInfo.tsx b/frontend/src/components/ChatBot/EntitiesInfo.tsx new file mode 100644 index 000000000..6c6e0784e --- /dev/null +++ b/frontend/src/components/ChatBot/EntitiesInfo.tsx @@ -0,0 +1,91 @@ +import { Box, GraphLabel, LoadingSpinner, Typography } from '@neo4j-ndl/react'; +import { FC, useMemo } from 'react'; +import { EntitiesProps, GroupedEntity } from '../../types'; +import { calcWordColor } from '@neo4j-devtools/word-color'; +import { graphLabels } from '../../utils/Constants'; +import { parseEntity } from '../../utils/Utils'; + +const EntitiesInfo: FC = ({ loading, mode, graphonly_entities, infoEntities }) => { + const groupedEntities = useMemo<{ [key: string]: GroupedEntity }>(() => { + const items = infoEntities.reduce((acc, entity) => { + const { label, text } = parseEntity(entity); + if (!acc[label]) { + const newColor = calcWordColor(label); + acc[label] = { texts: new Set(), color: newColor }; + } + acc[label].texts.add(text); + return acc; + }, {} as Record; color: string }>); + return items; + }, [infoEntities]); + + const labelCounts = useMemo(() => { + const counts: { [label: string]: number } = {}; + for (let index = 0; index < infoEntities?.length; index++) { + const entity = infoEntities[index]; + const { labels } = entity; + const [label] = labels; + counts[label] = counts[label] ? counts[label] + 1 : 1; + } + return counts; + }, [infoEntities]); + + const sortedLabels = useMemo(() => { + return Object.keys(labelCounts).sort((a, b) => labelCounts[b] - labelCounts[a]); + }, [labelCounts]); + return ( + <> + {loading ? ( + + + + ) : Object.keys(groupedEntities)?.length > 0 || Object.keys(graphonly_entities)?.length > 0 ? ( +
    + {mode == 'graph' + ? graphonly_entities.map((label, index) => ( +
  • +
    + { + // @ts-ignore + label[Object.keys(label)[0]].id ?? Object.keys(label)[0] + } +
    +
  • + )) + : sortedLabels.map((label, index) => { + const entity = groupedEntities[label == 'undefined' ? 'Entity' : label]; + return ( +
  • + e.preventDefault()} + > + {label === '__Community__' ? graphLabels.community : label} ({labelCounts[label]}) + + + {Array.from(entity.texts).slice(0, 3).join(', ')} + +
  • + ); + })} +
+ ) : ( + No Entities Found + )} + + ); +}; + +export default EntitiesInfo; diff --git a/frontend/src/components/ChatBot/SourcesInfo.tsx b/frontend/src/components/ChatBot/SourcesInfo.tsx new file mode 100644 index 000000000..fd5d89949 --- /dev/null +++ b/frontend/src/components/ChatBot/SourcesInfo.tsx @@ -0,0 +1,139 @@ +import { FC, useContext } from 'react'; +import { SourcesProps } from '../../types'; +import { Box, LoadingSpinner, TextLink, Typography } from '@neo4j-ndl/react'; +import { DocumentTextIconOutline, GlobeAltIconOutline } from '@neo4j-ndl/react/icons'; +import { getLogo, youtubeLinkValidation } from '../../utils/Utils'; +import { ThemeWrapperContext } from '../../context/ThemeWrapper'; +import HoverableLink from '../UI/HoverableLink'; +import wikipedialogo from '../../assets/images/wikipedia.svg'; +import youtubelogo from '../../assets/images/youtube.svg'; +import gcslogo from '../../assets/images/gcs.webp'; +import s3logo from '../../assets/images/s3logo.png'; + +const SourcesInfo: FC = ({ loading, mode, chunks, sources }) => { + const themeUtils = useContext(ThemeWrapperContext); + return ( + <> + {loading ? ( + + + + ) : mode === 'entity search+vector' && chunks?.length ? ( +
    + {chunks + .map((c) => ({ fileName: c.fileName, fileSource: c.fileSource })) + .map((s, index) => { + return ( +
  • +
    + {s.fileSource === 'local file' ? ( + + ) : ( + + )} + + {s.fileName} + +
    +
  • + ); + })} +
+ ) : sources?.length ? ( +
    + {sources.map((link, index) => { + return ( +
  • + {link?.startsWith('http') || link?.startsWith('https') ? ( + <> + {link?.includes('wikipedia.org') && ( +
    + Wikipedia Logo + + + + {link} + + + +
    + )} + {link?.includes('storage.googleapis.com') && ( +
    + Google Cloud Storage Logo + + {decodeURIComponent(link).split('/').at(-1)?.split('?')[0] ?? 'GCS File'} + +
    + )} + {youtubeLinkValidation(link) && ( + <> +
    + + + + + {link} + + + +
    + + )} + {!link?.startsWith('s3://') && + !link?.includes('storage.googleapis.com') && + !link?.includes('wikipedia.org') && + !link?.includes('youtube.com') && ( +
    + + + {link} + +
    + )} + + ) : link?.startsWith('s3://') ? ( +
    + S3 Logo + + {decodeURIComponent(link).split('/').at(-1) ?? 'S3 File'} + +
    + ) : ( +
    + + + {link} + +
    + )} +
  • + ); + })} +
+ ) : ( + No Sources Found + )} + + ); +}; + +export default SourcesInfo; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 679756dd9..9631335a9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -644,3 +644,27 @@ export interface DatabaseStatusProps { isGdsActive: boolean; uri: string | null; } + +export type SourcesProps = { + loading: boolean; + mode: string; + sources: string[]; + chunks: Chunk[]; +}; + +export type ChunkProps = { + loading: boolean; + chunks: Chunk[]; +}; + +export type EntitiesProps = { + loading: boolean; + mode: string; + graphonly_entities: []; + infoEntities: Entity[]; +}; + +export type CommunitiesProps = { + loading: boolean; + communities: Community[]; +}; diff --git a/frontend/src/utils/Utils.ts b/frontend/src/utils/Utils.ts index b59c082fd..82a7cda3e 100644 --- a/frontend/src/utils/Utils.ts +++ b/frontend/src/utils/Utils.ts @@ -449,3 +449,14 @@ export const getLogo = (mode: string): Record => { 'gcs bucket': gcslogo, }; }; + +export const generateYouTubeLink = (url: string, startTime: string) => { + try { + const urlObj = new URL(url); + urlObj.searchParams.set('t', startTime); + return urlObj.toString(); + } catch (error) { + console.error('Invalid URL:', error); + return ''; + } +};