diff --git a/package.json b/package.json index 1b2d272..c8a9fc8 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.4", + "@react-spring/web": "^9.7.5", "@supabase/supabase-js": "^2.46.2", "@tailwindcss/forms": "^0.5.9", "@tippyjs/react": "^4.2.6", @@ -61,6 +62,7 @@ "@visx/gradient": "^3.12.0", "@visx/grid": "^3.12.0", "@visx/group": "^3.12.0", + "@visx/hierarchy": "^3.12.0", "@visx/mock-data": "^3.12.0", "@visx/pattern": "^3.12.0", "@visx/responsive": "^3.12.0", @@ -69,6 +71,7 @@ "@visx/stats": "^3.12.0", "@visx/tooltip": "^3.12.0", "@visx/vendor": "^3.12.0", + "@visx/zoom": "^3.12.0", "ansi_up": "^6.0.2", "autoprefixer": "^10.4.20", "axios": "^1.7.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0af21bb..9b48695 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.4 version: 1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': + specifier: ^9.7.5 + version: 9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@supabase/supabase-js': specifier: ^2.46.2 version: 2.46.2 @@ -110,6 +113,9 @@ importers: '@visx/group': specifier: ^3.12.0 version: 3.12.0(react@18.3.1) + '@visx/hierarchy': + specifier: ^3.12.0 + version: 3.12.0(react@18.3.1) '@visx/mock-data': specifier: ^3.12.0 version: 3.12.0 @@ -134,6 +140,9 @@ importers: '@visx/vendor': specifier: ^3.12.0 version: 3.12.0 + '@visx/zoom': + specifier: ^3.12.0 + version: 3.12.0(react@18.3.1) ansi_up: specifier: ^6.0.2 version: 6.0.2 @@ -2422,6 +2431,33 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@react-spring/animated@9.7.5': + resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.7.5': + resolution: {integrity: sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.7.5': + resolution: {integrity: sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==} + + '@react-spring/shared@9.7.5': + resolution: {integrity: sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/types@9.7.5': + resolution: {integrity: sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==} + + '@react-spring/web@9.7.5': + resolution: {integrity: sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@react-stately/calendar@3.5.1': resolution: {integrity: sha512-7l7QhqGUJ5AzWHfvZzbTe3J4t72Ht5BmhW4hlVI7flQXtfrmYkVtl3ZdytEZkkHmWGYZRW9b4IQTQGZxhtlElA==} peerDependencies: @@ -2875,6 +2911,9 @@ packages: '@types/d3-geo@3.1.0': resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + '@types/d3-hierarchy@1.1.11': + resolution: {integrity: sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==} + '@types/d3-hierarchy@3.1.7': resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} @@ -3138,6 +3177,14 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@visx/annotation@3.12.0': resolution: {integrity: sha512-ZH6Y4jfrb47iEUV9O2itU9TATE5IPzhs5qvP6J7vmv26qkqwDcuE7xN3S3l9R70WjyEKGbpO8js4EijA3FJWkA==} peerDependencies: @@ -3185,6 +3232,11 @@ packages: peerDependencies: react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + '@visx/hierarchy@3.12.0': + resolution: {integrity: sha512-+X1HOeLEOODxjAD7ixrWJ4KCVei4wFe8ra3dYU0uZ14RdPPgUeiuyBfdeXWZuAHM6Ix9qrryneatQjkC3h4mvA==} + peerDependencies: + react: ^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0 + '@visx/mock-data@3.12.0': resolution: {integrity: sha512-HI8LKdO3sU2tIBv16ZYRTc2JYsu0Ai/hQc7YUOBqbjhXUW993iCBe98pAgEdHDrSWqK2yvXY4En5ceBTAP34Jw==} @@ -3228,6 +3280,11 @@ packages: '@visx/vendor@3.12.0': resolution: {integrity: sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==} + '@visx/zoom@3.12.0': + resolution: {integrity: sha512-JmxkOROPkjnMEdFGnnSKLo5BkFHgOkLe/N5KkWR02cA5bE+bmEkfAh7DJfrtVsPkqSPvwGH1TrMWWthJwoivPA==} + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -3833,6 +3890,9 @@ packages: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} + d3-hierarchy@1.1.9: + resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==} + d3-hierarchy@3.1.2: resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} engines: {node: '>=12'} @@ -9922,6 +9982,38 @@ snapshots: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-spring/animated@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/core@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/rafz@9.7.5': {} + + '@react-spring/shared@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/rafz': 9.7.5 + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/types@9.7.5': {} + + '@react-spring/web@9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/core': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@react-stately/calendar@3.5.1(react@18.3.1)': dependencies: '@internationalized/date': 3.6.0 @@ -10497,6 +10589,8 @@ snapshots: dependencies: '@types/geojson': 7946.0.14 + '@types/d3-hierarchy@1.1.11': {} + '@types/d3-hierarchy@3.1.7': {} '@types/d3-interpolate@3.0.1': @@ -10818,6 +10912,13 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + '@visx/annotation@3.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@types/react': 18.3.3 @@ -10904,6 +11005,16 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 + '@visx/hierarchy@3.12.0(react@18.3.1)': + dependencies: + '@types/d3-hierarchy': 1.1.11 + '@types/react': 18.3.3 + '@visx/group': 3.12.0(react@18.3.1) + classnames: 2.5.1 + d3-hierarchy: 1.1.9 + prop-types: 15.8.1 + react: 18.3.1 + '@visx/mock-data@3.12.0': dependencies: '@types/d3-random': 2.2.3 @@ -10999,6 +11110,14 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 + '@visx/zoom@3.12.0(react@18.3.1)': + dependencies: + '@types/react': 18.3.3 + '@use-gesture/react': 10.3.1(react@18.3.1) + '@visx/event': 3.12.0 + prop-types: 15.8.1 + react: 18.3.1 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -11649,6 +11768,8 @@ snapshots: dependencies: d3-array: 3.2.4 + d3-hierarchy@1.1.9: {} + d3-hierarchy@3.1.2: {} d3-interpolate@3.0.1: diff --git a/src/components/ChatDemo/ExtractContainer.tsx b/src/components/ChatDemo/ExtractContainer.tsx index fe44703..86d82f0 100644 --- a/src/components/ChatDemo/ExtractContainer.tsx +++ b/src/components/ChatDemo/ExtractContainer.tsx @@ -25,7 +25,8 @@ const ExtractButtonContainer: React.FC = ({ const { getClient } = useUserContext(); const isIngestionValid = () => { - return ingestionStatus === 'SUCCESS' || ingestionStatus === 'ENRICHED'; + const status = ingestionStatus.toUpperCase(); + return status === 'SUCCESS'; }; const handleDocumentExtraction = async () => { diff --git a/src/components/knowledgeGraph.tsx b/src/components/knowledgeGraph.tsx new file mode 100644 index 0000000..c94e621 --- /dev/null +++ b/src/components/knowledgeGraph.tsx @@ -0,0 +1,202 @@ +import { animated, useTransition, interpolate } from '@react-spring/web'; +import { Group } from '@visx/group'; +import { scaleOrdinal } from '@visx/scale'; +import Pie, { ProvidedProps, PieArcDatum } from '@visx/shape/lib/shapes/Pie'; +import { EntityResponse } from 'r2r-js/dist/types'; +import React from 'react'; + +const MIN_PERCENTAGE_THRESHOLD = 0.02; + +interface PieData { + category: string; + count: number; +} + +interface CategoryCount { + category: string; + count: number; +} + +const defaultMargin = { top: 20, right: 20, bottom: 20, left: 20 }; + +interface KnowledgeGraphProps { + entities: EntityResponse[]; + width: number; + height: number; + margin?: typeof defaultMargin; +} + +export default function KnowledgeGraph({ + entities, + width, + height, + margin = defaultMargin, +}: KnowledgeGraphProps) { + if (width < 10 || height < 10) { + return null; + } + + // Process entities to get category counts + const categoryMap = new Map(); + entities.forEach((entity) => { + const category = entity.category || 'Uncategorized'; + categoryMap.set(category, (categoryMap.get(category) || 0) + 1); + }); + + const total = Array.from(categoryMap.values()).reduce( + (sum, count) => sum + count, + 0 + ); + const categoryData: CategoryCount[] = []; + let otherCount = 0; + + Array.from(categoryMap.entries()).forEach(([category, count]) => { + const percentage = count / total; + if (percentage >= MIN_PERCENTAGE_THRESHOLD) { + categoryData.push({ category, count }); + } else { + otherCount += count; + } + }); + + if (otherCount > 0) { + categoryData.push({ category: 'Other', count: otherCount }); + } + + // Create color scale + const getColor = scaleOrdinal({ + domain: categoryData.map((d) => d.category), + range: [ + '#FF6B6B', + '#4ECDC4', + '#45B7D1', + '#96CEB4', + '#FFEEAD', + '#D4A5A5', + '#9B786F', + '#A8E6CF', + ], + }); + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + const radius = Math.min(innerWidth, innerHeight) / 2; + const centerY = innerHeight / 2; + const centerX = innerWidth / 2; + + return ( +
+ {/* Text overlay - positioned absolutely in top left */} +
+

+ Distribution of Entity Categories +

+
+ Breaks down the types of entities in your collection +
+
+ + {/* Centered pie chart */} + + + data.count} + outerRadius={radius} + innerRadius={radius * 0.6} + cornerRadius={3} + padAngle={0.02} + > + {(pie) => ( + arc.data.category} + getColor={(arc) => getColor(arc.data.category)} + /> + )} + + + +
+ ); +} + +type AnimatedStyles = { startAngle: number; endAngle: number; opacity: number }; + +const fromLeaveTransition = () => ({ + startAngle: 0, + endAngle: 2 * Math.PI, + opacity: 1, +}); + +const enterUpdateTransition = ({ startAngle, endAngle }: PieArcDatum) => ({ + startAngle, + endAngle, + opacity: 1, +}); + +type AnimatedPieProps = ProvidedProps & { + getKey: (d: PieArcDatum) => string; + getColor: (d: PieArcDatum) => string; +}; + +function AnimatedPie({ + arcs, + path, + getKey, + getColor, +}: AnimatedPieProps) { + const transitions = useTransition, AnimatedStyles>( + arcs, + { + from: fromLeaveTransition, + enter: enterUpdateTransition, + update: enterUpdateTransition, + leave: fromLeaveTransition, + keys: getKey, + config: { + duration: 500, + tension: 120, + friction: 14, + }, + } + ); + + return transitions((props, arc, { key }) => { + const [centroidX, centroidY] = path.centroid(arc); + const percentage = (arc.endAngle - arc.startAngle) / (2 * Math.PI); + const hasSpaceForLabel = percentage >= 0.02; + + return ( + + + path({ + ...arc, + startAngle, + endAngle, + }) + )} + fill={getColor(arc)} + /> + {hasSpaceForLabel && ( + + + {`${getKey(arc)} (${arc.data.count})`} + + + )} + + ); + }); +} diff --git a/src/pages/collections/[id].tsx b/src/pages/collections/[id].tsx index 663f13d..be7603a 100644 --- a/src/pages/collections/[id].tsx +++ b/src/pages/collections/[id].tsx @@ -8,12 +8,19 @@ import { RelationshipResponse, User, } from 'r2r-js/dist/types'; -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { + useRef, + useState, + useEffect, + useCallback, + useMemo, +} from 'react'; import { RemoveButton } from '@/components/ChatDemo/remove'; import Table, { Column } from '@/components/ChatDemo/Table'; import CollectionDialog from '@/components/ChatDemo/utils/collectionDialog'; import DocumentInfoDialog from '@/components/ChatDemo/utils/documentDialogInfo'; +import KnowledgeGraph from '@/components/knowledgeGraph'; import Layout from '@/components/Layout'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/Button'; @@ -30,6 +37,12 @@ const CollectionIdPage: React.FC = () => { const router = useRouter(); const { getClient } = useUserContext(); + const [containerDimensions, setContainerDimensions] = useState({ + width: 0, + height: 0, + }); + const graphContainerRef = useRef(null); + const [collection, setCollection] = useState(null); const [documents, setDocuments] = useState([]); @@ -89,6 +102,31 @@ const CollectionIdPage: React.FC = () => { currentCollectionId ); + useEffect(() => { + const updateDimensions = () => { + if (graphContainerRef.current && activeTab === 'viewEntities') { + const width = graphContainerRef.current.offsetWidth; + const height = graphContainerRef.current.offsetHeight; + setContainerDimensions({ + width, + height, + }); + } + }; + + updateDimensions(); + + // Small delay to ensure the tab content is rendered + const timeoutId = setTimeout(updateDimensions, 100); + + window.addEventListener('resize', updateDimensions); + + return () => { + window.removeEventListener('resize', updateDimensions); + clearTimeout(timeoutId); + }; + }, [activeTab]); // Add activeTab as dependency + const fetchCollection = useCallback(async () => { if (!currentCollectionId) { return; @@ -762,7 +800,7 @@ const CollectionIdPage: React.FC = () => { onValueChange={setActiveTab} className="flex flex-col flex-1 mt-4 overflow-hidden" > - + Documents @@ -778,6 +816,9 @@ const CollectionIdPage: React.FC = () => { Communities + + Explore +
@@ -883,6 +924,20 @@ const CollectionIdPage: React.FC = () => { showPagination={true} /> + +
+ {containerDimensions.width > 0 && entities.length > 0 && ( + + )} +
+
{ // Fetch first batch const firstBatch = await client.documents.list({ offset: offset, - limit: 100, + limit: 1000, }); console.log('firstBatch:', firstBatch);