Skip to content

Graph communities #748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 18, 2024
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') ? (
<>
<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') ? (
<>
<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') &&
!chunk?.url.includes('wikipedia.org') &&
!chunk?.url.includes('youtube.com') ? (
<>
<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