Skip to content

Commit

Permalink
Graph communities (#748)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
prakriti-solankey and kartikpersistent authored Sep 18, 2024
1 parent d6b2e6a commit 1695f19
Show file tree
Hide file tree
Showing 10 changed files with 508 additions and 402 deletions.
3 changes: 3 additions & 0 deletions backend/src/chunkid_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -385,5 +385,5 @@

.custom-menu {
min-width: 250px;
max-width: 300px;
max-width: 305px;
}
413 changes: 39 additions & 374 deletions frontend/src/components/ChatBot/ChatInfoModal.tsx

Large diffs are not rendered by default.

64 changes: 37 additions & 27 deletions frontend/src/components/ChatBot/ChatModeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,42 +19,52 @@ 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(() => {
return isGdsActive && isCommunityAllowed
? chatModes
: chatModes?.filter((m) => !m.mode.includes('entity search+vector'));
}, [isGdsActive, isCommunityAllowed]);


const menuItems = useMemo(() => {
return memoizedChatModes?.map((m) => ({
title: (
<div>
<Typography variant='subheading-small'>
{m.mode.includes('+') ? capitalizeWithPlus(m.mode) : capitalize(m.mode)}
</Typography>
return memoizedChatModes?.map((m) => {
const isDisabled = Boolean(selectedRows.length && !(m.mode === 'vector' || m.mode === 'graph+vector'));
return {
title: (
<div>
<Typography variant='body-small'>{m.description}</Typography>
<Typography variant='subheading-small'>
{m.mode.includes('+') ? capitalizeWithPlus(m.mode) : capitalize(m.mode)}
</Typography>
<div>
<Typography variant='body-small'>{m.description}</Typography>
</div>
</div>
</div>
),
onClick: () => {
setchatMode(m.mode);
closeHandler(); // Close the menu after setting the chat mode
},
disabledCondition: false,
description: (
<span>
{chatMode === m.mode && (
<>
<StatusIndicator type='success' /> Selected
</>
)}
</span>
),
}));
}, [chatMode, memoizedChatModes, setchatMode, closeHandler]);
),
onClick: () => {
setchatMode(m.mode);
closeHandler();
},
disabledCondition: isDisabled,
description: (
<span>
{chatMode === m.mode && (
<>
<StatusIndicator type='success' /> Selected
</>
)}
{isDisabled && (
<>
<StatusIndicator type='warning' /> Chatmode not available
</>
)}
</span>
),
};
});
}, [chatMode, memoizedChatModes, setchatMode, closeHandler, selectedRows]);
return (
<CustomMenu
closeHandler={closeHandler}
Expand Down
127 changes: 127 additions & 0 deletions frontend/src/components/ChatBot/ChunkInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { FC, useContext } from 'react';
import { ChunkProps } from '../../types';
import { Box, LoadingSpinner, TextLink, Typography } from '@neo4j-ndl/react';
import { DocumentTextIconOutline, GlobeAltIconOutline } from '@neo4j-ndl/react/icons';
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 ReactMarkdown from 'react-markdown';
import { generateYouTubeLink, getLogo } from '../../utils/Utils';
import { ThemeWrapperContext } from '../../context/ThemeWrapper';

const ChunkInfo: FC<ChunkProps> = ({ loading, chunks }) => {
const themeUtils = useContext(ThemeWrapperContext);

return (
<>
{loading ? (
<Box className='flex justify-center items-center'>
<LoadingSpinner size='small' />
</Box>
) : chunks?.length > 0 ? (
<div className='p-4 h-80 overflow-auto'>
<ul className='list-disc list-inside'>
{chunks.map((chunk) => (
<li key={chunk.id} className='mb-2'>
{chunk?.page_number ? (
<>
<div className='flex flex-row inline-block justiy-between items-center'>
<DocumentTextIconOutline className='w-4 h-4 inline-block mr-2' />
<Typography
variant='subheading-medium'
className='text-ellipsis whitespace-nowrap max-w-[calc(100%-200px)] overflow-hidden'
>
{chunk?.fileName}
</Typography>
</div>
<Typography variant='subheading-small'>Similarity Score: {chunk?.score}</Typography>
</>
) : chunk?.url && chunk?.start_time ? (
<>
<div className='flex flex-row inline-block justiy-between items-center'>
<img src={youtubelogo} width={20} height={20} className='mr-2' />
<TextLink href={generateYouTubeLink(chunk?.url, chunk?.start_time)} externalLink={true}>
<Typography
variant='body-medium'
className='text-ellipsis whitespace-nowrap overflow-hidden max-w-lg'
>
{chunk?.fileName}
</Typography>
</TextLink>
</div>
<Typography variant='subheading-small'>Similarity Score: {chunk?.score}</Typography>
</>
) : chunk?.url && chunk?.url.includes('wikipedia.org') ? (

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
wikipedia.org
' can be anywhere in the URL, and arbitrary hosts may come before or after it.
<>
<div className='flex flex-row inline-block justiy-between items-center'>
<img src={wikipedialogo} width={20} height={20} className='mr-2' />
<Typography variant='subheading-medium'>{chunk?.fileName}</Typography>
</div>
<Typography variant='subheading-small'>Similarity Score: {chunk?.score}</Typography>
</>
) : chunk?.url && chunk?.url.includes('storage.googleapis.com') ? (

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
storage.googleapis.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.
<>
<div className='flex flex-row inline-block justiy-between items-center'>
<img src={gcslogo} width={20} height={20} className='mr-2' />
<Typography variant='subheading-medium'>{chunk?.fileName}</Typography>
</div>
<Typography variant='subheading-small'>Similarity Score: {chunk?.score}</Typography>
</>
) : chunk?.url && chunk?.url.startsWith('s3://') ? (
<>
<div className='flex flex-row inline-block justiy-between items-center'>
<img src={s3logo} width={20} height={20} className='mr-2' />
<Typography variant='subheading-medium'>{chunk?.fileName}</Typography>
</div>
<Typography variant='subheading-small'>Similarity Score: {chunk?.score}</Typography>
</>
) : chunk?.url &&
!chunk?.url.startsWith('s3://') &&
!chunk?.url.includes('storage.googleapis.com') &&

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
storage.googleapis.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.
!chunk?.url.includes('wikipedia.org') &&

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
wikipedia.org
' can be anywhere in the URL, and arbitrary hosts may come before or after it.
!chunk?.url.includes('youtube.com') ? (

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
youtube.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.
<>
<div className='flex flex-row inline-block items-center'>
<GlobeAltIconOutline className='n-size-token-7' />
<TextLink href={chunk?.url} externalLink={true}>
<Typography variant='body-medium'>{chunk?.url}</Typography>
</TextLink>
</div>
<Typography variant='subheading-small'>Similarity Score: {chunk?.score}</Typography>
</>
) : (
<>
<div className='flex flex-row inline-block items-center'>
{chunk.fileSource === 'local file' ? (
<DocumentTextIconOutline className='n-size-token-7 mr-2' />
) : (
<img
src={getLogo(themeUtils.colorMode)[chunk.fileSource]}
width={20}
height={20}
className='mr-2'
/>
)}
<Typography
variant='body-medium'
className='text-ellipsis whitespace-nowrap overflow-hidden max-w-lg'
>
{chunk.fileName}
</Typography>
</div>
</>
)}
<ReactMarkdown>{chunk?.text}</ReactMarkdown>
</li>
))}
</ul>
</div>
) : (
<span className='h6 text-center'> No Chunks Found</span>
)}
</>
);
};

export default ChunkInfo;
36 changes: 36 additions & 0 deletions frontend/src/components/ChatBot/Communities.tsx
Original file line number Diff line number Diff line change
@@ -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<CommunitiesProps> = ({ loading, communities }) => {
return (
<>
{loading ? (
<Box className='flex justify-center items-center'>
<LoadingSpinner size='small' />
</Box>
) : communities?.length > 0 ? (
<div className='p-4 h-80 overflow-auto'>
<ul className='list-disc list-inside'>
{communities.map((community, index) => (
<li key={`${community.id}${index}`} className='mb-2'>
<div>
<Flex flexDirection='row' gap='2'>
<Typography variant='subheading-medium'>ID : </Typography>
<Typography variant='subheading-medium'>{community.id}</Typography>
</Flex>
<ReactMarkdown>{community.summary}</ReactMarkdown>
</div>
</li>
))}
</ul>
</div>
) : (
<span className='h6 text-center'> No Communities Found</span>
)}
</>
);
};

export default CommunitiesInfo;
91 changes: 91 additions & 0 deletions frontend/src/components/ChatBot/EntitiesInfo.tsx
Original file line number Diff line number Diff line change
@@ -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<EntitiesProps> = ({ 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<string, { texts: Set<string>; 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 ? (
<Box className='flex justify-center items-center'>
<LoadingSpinner size='small' />
</Box>
) : Object.keys(groupedEntities)?.length > 0 || Object.keys(graphonly_entities)?.length > 0 ? (
<ul className='list-none p-4 max-h-80 overflow-auto'>
{mode == 'graph'
? graphonly_entities.map((label, index) => (
<li
key={index}
className='flex items-center mb-2 text-ellipsis whitespace-nowrap max-w-[100%)] overflow-hidden'
>
<div style={{ backgroundColor: calcWordColor(Object.keys(label)[0]) }} className='legend mr-2'>
{
// @ts-ignore
label[Object.keys(label)[0]].id ?? Object.keys(label)[0]
}
</div>
</li>
))
: sortedLabels.map((label, index) => {
const entity = groupedEntities[label == 'undefined' ? 'Entity' : label];
return (
<li
key={index}
className='flex items-center mb-2 text-ellipsis whitespace-nowrap max-w-[100%)] overflow-hidden'
>
<GraphLabel
type='node'
className='legend'
color={`${entity.color}`}
selected={false}
onClick={(e) => e.preventDefault()}
>
{label === '__Community__' ? graphLabels.community : label} ({labelCounts[label]})
</GraphLabel>
<Typography
className='ml-2 text-ellipsis whitespace-nowrap max-w-[calc(100%-120px)] overflow-hidden'
variant='body-medium'
>
{Array.from(entity.texts).slice(0, 3).join(', ')}
</Typography>
</li>
);
})}
</ul>
) : (
<span className='h6 text-center'>No Entities Found</span>
)}
</>
);
};

export default EntitiesInfo;
Loading

0 comments on commit 1695f19

Please sign in to comment.