├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public ├── apple-touch-icon.png ├── artifacts │ └── put_parquet_files_here ├── demo.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── search.png ├── src ├── app │ ├── api │ │ └── agent.ts │ ├── components │ │ ├── APISearchDrawer.tsx │ │ ├── DataTable.tsx │ │ ├── DataTableContainer.tsx │ │ ├── DetailDrawer.tsx │ │ ├── DropZone.tsx │ │ ├── GraphDataHandler.tsx │ │ ├── GraphViewer.tsx │ │ ├── Introduction.tsx │ │ └── SearchDrawer.tsx │ ├── hooks │ │ ├── useFileHandler.ts │ │ └── useGraphData.ts │ ├── layout │ │ └── App.tsx │ ├── models │ │ ├── community-report.ts │ │ ├── community.ts │ │ ├── covariate.ts │ │ ├── custom-graph-data.ts │ │ ├── document.ts │ │ ├── entity.ts │ │ ├── relationship.ts │ │ ├── search-result.ts │ │ └── text-unit.ts │ └── utils │ │ └── parquet-utils.ts ├── declarations.d.ts ├── hyparquet.d.ts ├── index.tsx ├── react-app-env.d.ts ├── react-table-config.d.ts ├── reportWebVitals.ts └── setupTests.ts └── tsconfig.json /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: "16" # Specify the Node.js version you need 20 | 21 | - name: Install dependencies 22 | run: npm install 23 | 24 | - name: Configure Git 25 | run: | 26 | git config --global user.name "github-actions[bot]" 27 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 28 | 29 | - name: Deploy to GitHub Pages 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 32 | REACT_APP_GA_MEASUREMENT_ID: ${{ secrets.REACT_APP_GA_MEASUREMENT_ID }} 33 | CI: false 34 | run: | 35 | git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }} 36 | npm run deploy 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | 27 | /public/artifacts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Yan-Ying Liao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphRAG Visualizer 2 | 3 | 👉 [GraphRAG Visualizer](https://noworneverev.github.io/graphrag-visualizer/)
4 | 👉 [GraphRAG Visualizer Demo](https://www.youtube.com/watch?v=Hjx1iTZZtzw) 5 | 6 | ![demo](public/demo.png) 7 | 8 | ## Overview 9 | 10 | GraphRAG Visualizer is an application designed to visualize Microsoft [GraphRAG](https://github.com/microsoft/graphrag) artifacts. By uploading parquet files generated from the GraphRAG indexing pipeline, users can easily view and analyze data without needing additional software or scripts. 11 | 12 | ## Important Note 13 | 14 | If you are using **GraphRAG 0.3.x or below**, please use the legacy version of GraphRAG Visualizer available at: 15 | 👉 [GraphRAG Visualizer Legacy](https://noworneverev.github.io/graphrag-visualizer-legacy) 16 | 17 | ## Features 18 | 19 | - **Graph Visualization**: View the graph in 2D or 3D in the "Graph Visualization" tab. 20 | - **Data Tables**: Display data from the parquet files in the "Data Tables" tab. 21 | - **Search Functionality**: Fully supports search, allowing users to focus on specific nodes or relationships. 22 | - **Local Processing**: All artifacts are processed locally on your machine, ensuring data security and privacy. 23 | 24 | ## Using the Search Functionality 25 | 26 | Once the [graphrag-api](https://github.com/noworneverev/graphrag-api) server is up and running, you can perform searches directly through the GraphRAG Visualizer. Simply go to the [GraphRAG Visualizer](https://noworneverev.github.io/graphrag-visualizer/) and use the search interface to query the API server. This allows you to easily search and explore data that is hosted on your local server. 27 | 28 | ![search](public/search.png) 29 | 30 | ## Graph Data Model 31 | 32 | The logic for creating relationships for text units, documents, communities, and covariates is derived from the [GraphRAG import Neo4j Cypher notebook](https://github.com/microsoft/graphrag/blob/main/examples_notebooks/community_contrib/neo4j/graphrag_import_neo4j_cypher.ipynb). 33 | 34 | ### Nodes 35 | 36 | | Node | Type | 37 | | --------- | -------------- | 38 | | Document | `RAW_DOCUMENT` | 39 | | Text Unit | `CHUNK` | 40 | | Community | `COMMUNITY` | 41 | | Finding | `FINDING` | 42 | | Covariate | `COVARIATE` | 43 | | Entity | _Varies_ | 44 | 45 | ### Relationships 46 | 47 | | Source Node | Relationship | Target Node | 48 | | ----------- | --------------- | ----------- | 49 | | Entity | `RELATED` | Entity | 50 | | Text Unit | `PART_OF` | Document | 51 | | Text Unit | `HAS_ENTITY` | Entity | 52 | | Text Unit | `HAS_COVARIATE` | Covariate | 53 | | Community | `HAS_FINDING` | Finding | 54 | | Entity | `IN_COMMUNITY` | Community | 55 | 56 | ## Developer Instructions 57 | 58 | ### Setting Up the Project 59 | 60 | 1. Clone the repository to your local machine: 61 | 62 | ```bash 63 | git clone https://github.com/noworneverev/graphrag-visualizer.git 64 | cd graphrag-visualizer 65 | ``` 66 | 67 | 2. Install the necessary dependencies: 68 | 69 | ```bash 70 | npm install 71 | ``` 72 | 73 | 3. Run the development server: 74 | 75 | ```bash 76 | npm start 77 | ``` 78 | 79 | 4. Open the app in your browser: 80 | ``` 81 | http://localhost:3000 82 | ``` 83 | 84 | ### Loading Parquet Files 85 | 86 | To load `.parquet` files automatically when the application starts, place your Parquet files in the `public/artifacts` directory. These files will be loaded into the application for visualization and data table display. The files can be organized as follows: 87 | 88 | - GraphRAG v2.x.x 89 | - `public/artifacts/entities.parquet` 90 | - `public/artifacts/relationships.parquet` 91 | - `public/artifacts/documents.parquet` 92 | - `public/artifacts/text_units.parquet` 93 | - `public/artifacts/communities.parquet` 94 | - `public/artifacts/community_reports.parquet` 95 | - `public/artifacts/covariates.parquet` 96 | 97 | - GraphRAG v1.x.x 98 | - `public/artifacts/create_final_entities.parquet` 99 | - `public/artifacts/create_final_relationships.parquet` 100 | - `public/artifacts/create_final_documents.parquet` 101 | - `public/artifacts/create_final_text_units.parquet` 102 | - `public/artifacts/create_final_communities.parquet` 103 | - `public/artifacts/create_final_community_reports.parquet` 104 | - `public/artifacts/create_final_covariates.parquet` 105 | 106 | If the files are placed in the `public/artifacts` folder, the app will automatically load and display them on startup. 107 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = function override(config) { 2 | config.resolve.fallback = { 3 | fs: false, 4 | }; 5 | 6 | return config; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphrag-visualizer", 3 | "version": "0.1.0", 4 | "homepage": "https://noworneverev.github.io/graphrag-visualizer/", 5 | "private": true, 6 | "dependencies": { 7 | "@emotion/react": "^11.13.0", 8 | "@emotion/styled": "^11.13.0", 9 | "@mui/icons-material": "^5.16.5", 10 | "@mui/material": "^5.16.5", 11 | "@testing-library/jest-dom": "^5.17.0", 12 | "@testing-library/react": "^13.4.0", 13 | "@testing-library/user-event": "^13.5.0", 14 | "@types/jest": "^27.5.2", 15 | "@types/node": "^16.18.104", 16 | "@types/react": "^18.3.3", 17 | "@types/react-dom": "^18.3.0", 18 | "axios": "^1.7.2", 19 | "fuse.js": "^7.0.0", 20 | "hyparquet": "^1.6.4", 21 | "material-react-table": "^2.13.1", 22 | "react": "^18.3.1", 23 | "react-app-rewired": "^2.2.1", 24 | "react-dom": "^18.3.1", 25 | "react-dropzone": "^14.2.3", 26 | "react-force-graph-2d": "^1.25.5", 27 | "react-force-graph-3d": "^1.24.3", 28 | "react-ga4": "^2.1.0", 29 | "react-router-dom": "^6.27.0", 30 | "react-scripts": "5.0.1", 31 | "react-table": "^7.8.0", 32 | "three": "^0.167.1", 33 | "three-spritetext": "^1.8.2", 34 | "typescript": "^4.9.5", 35 | "web-vitals": "^2.1.4" 36 | }, 37 | "scripts": { 38 | "predeploy": "npm run build", 39 | "deploy": "gh-pages -d build", 40 | "start": "react-app-rewired start", 41 | "build": "react-app-rewired build", 42 | "test": "react-app-rewired test", 43 | "eject": "react-scripts eject" 44 | }, 45 | "eslintConfig": { 46 | "extends": [ 47 | "react-app", 48 | "react-app/jest" 49 | ] 50 | }, 51 | "browserslist": { 52 | "production": [ 53 | ">0.2%", 54 | "not dead", 55 | "not op_mini all" 56 | ], 57 | "development": [ 58 | "last 1 chrome version", 59 | "last 1 firefox version", 60 | "last 1 safari version" 61 | ] 62 | }, 63 | "devDependencies": { 64 | "@types/react-table": "^7.7.20", 65 | "@types/three": "^0.167.1", 66 | "gh-pages": "^6.1.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noworneverev/graphrag-visualizer/0a78b47ff2568f2c5b94867116ae2500d263ee22/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/artifacts/put_parquet_files_here: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noworneverev/graphrag-visualizer/0a78b47ff2568f2c5b94867116ae2500d263ee22/public/artifacts/put_parquet_files_here -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noworneverev/graphrag-visualizer/0a78b47ff2568f2c5b94867116ae2500d263ee22/public/demo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noworneverev/graphrag-visualizer/0a78b47ff2568f2c5b94867116ae2500d263ee22/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | GraphRAG Visualizer 30 | 31 | 32 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noworneverev/graphrag-visualizer/0a78b47ff2568f2c5b94867116ae2500d263ee22/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noworneverev/graphrag-visualizer/0a78b47ff2568f2c5b94867116ae2500d263ee22/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "GraphRAG Viz", 3 | "name": "GraphRAG Visualizer", 4 | "description": "Visualize Microsoft GraphRAG artifacts by uploading parquet files.", 5 | "icons": [ 6 | { 7 | "src": "favicon.ico", 8 | "sizes": "64x64 32x32 24x24 16x16", 9 | "type": "image/x-icon" 10 | }, 11 | { 12 | "src": "logo192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "logo512.png", 18 | "type": "image/png", 19 | "sizes": "512x512" 20 | } 21 | ], 22 | "start_url": ".", 23 | "display": "standalone", 24 | "theme_color": "#000000", 25 | "background_color": "#ffffff" 26 | } 27 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noworneverev/graphrag-visualizer/0a78b47ff2568f2c5b94867116ae2500d263ee22/public/search.png -------------------------------------------------------------------------------- /src/app/api/agent.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosResponse } from 'axios'; 2 | 3 | // Configure the base URL for the Axios instance 4 | axios.defaults.baseURL = process.env.REACT_APP_API_URL || 'http://localhost:8000'; 5 | axios.defaults.withCredentials = true; 6 | 7 | const responseBody = (response: AxiosResponse) => response.data; 8 | 9 | // Intercept responses to handle errors globally 10 | axios.interceptors.response.use( 11 | async (response) => { 12 | return response; 13 | }, 14 | (error: AxiosError) => { 15 | const { data, status } = error.response!; 16 | switch (status) { 17 | case 400: 18 | if (data.errors) { 19 | const modelStateErrors: string[] = []; 20 | for (const key in data.errors) { 21 | if (data.errors[key]) { 22 | modelStateErrors.push(data.errors[key]); 23 | } 24 | } 25 | console.error('Validation errors:', modelStateErrors.flat()); 26 | } else { 27 | console.error('Bad request:', data.title); 28 | } 29 | break; 30 | case 401: 31 | console.error('Unauthorized:', data.title || 'Unauthorized'); 32 | break; 33 | case 403: 34 | console.error('Forbidden: You are not allowed to do that!'); 35 | break; 36 | case 500: 37 | console.error('Server Error:', data.title || 'Server Error!'); 38 | break; 39 | default: 40 | console.error('An unexpected error occurred.'); 41 | break; 42 | } 43 | return Promise.reject(error.response); 44 | } 45 | ); 46 | 47 | const requests = { 48 | get: (url: string, params?: URLSearchParams) => 49 | axios.get(url, { params }).then(responseBody), 50 | post: (url: string, body: {}) => axios.post(url, body).then(responseBody), 51 | put: (url: string, body: {}) => axios.put(url, body).then(responseBody), 52 | delete: (url: string) => axios.delete(url).then(responseBody), 53 | }; 54 | 55 | const Search = { 56 | global: (query: string) => requests.get('search/global', new URLSearchParams({ query })), 57 | local: (query: string) => requests.get('search/local', new URLSearchParams({ query })), 58 | }; 59 | 60 | const Status = { 61 | check: () => requests.get('status'), 62 | }; 63 | 64 | const agent = { 65 | Search, 66 | Status, 67 | }; 68 | 69 | export default agent; 70 | -------------------------------------------------------------------------------- /src/app/components/APISearchDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | Box, 4 | Button, 5 | CircularProgress, 6 | Drawer, 7 | TextField, 8 | Typography, 9 | Card, 10 | CardContent, 11 | CardHeader, 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableContainer, 16 | TableHead, 17 | TableRow, 18 | Paper, 19 | IconButton, 20 | Collapse, 21 | Link, 22 | Alert, 23 | } from "@mui/material"; 24 | import CloseIcon from "@mui/icons-material/Close"; 25 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; 26 | import ExpandLessIcon from "@mui/icons-material/ExpandLess"; 27 | import { SearchResult } from "../models/search-result"; 28 | 29 | interface APISearchDrawerProps { 30 | apiDrawerOpen: boolean; 31 | toggleDrawer: (open: boolean) => () => void; 32 | handleApiSearch: ( 33 | query: string, 34 | searchType: "local" | "global" 35 | ) => Promise; 36 | apiSearchResults: SearchResult | null; 37 | localSearchEnabled: boolean; 38 | globalSearchEnabled: boolean; 39 | hasCovariates: boolean; 40 | serverUp: boolean; 41 | } 42 | 43 | const APISearchDrawer: React.FC = ({ 44 | apiDrawerOpen, 45 | toggleDrawer, 46 | handleApiSearch, 47 | apiSearchResults, 48 | localSearchEnabled, 49 | globalSearchEnabled, 50 | hasCovariates, 51 | serverUp, 52 | }) => { 53 | const [query, setQuery] = useState(""); 54 | const [loadingLocal, setLoadingLocal] = useState(false); 55 | const [loadingGlobal, setLoadingGlobal] = useState(false); 56 | const [expandedTables, setExpandedTables] = useState<{ 57 | [key: string]: boolean; 58 | }>({}); 59 | 60 | useEffect(() => { 61 | // Initialize the expandedTables state to false for all keys in context_data 62 | if (apiSearchResults && apiSearchResults.context_data) { 63 | const initialExpandedState: { [key: string]: boolean } = {}; 64 | Object.keys(apiSearchResults.context_data).forEach((key) => { 65 | initialExpandedState[key] = true; 66 | }); 67 | setExpandedTables(initialExpandedState); 68 | } 69 | }, [apiSearchResults]); 70 | 71 | const handleSearch = async (searchType: "local" | "global") => { 72 | if (searchType === "local") { 73 | setLoadingLocal(true); 74 | } else { 75 | setLoadingGlobal(true); 76 | } 77 | 78 | try { 79 | await handleApiSearch(query, searchType); 80 | } finally { 81 | if (searchType === "local") { 82 | setLoadingLocal(false); 83 | } else { 84 | setLoadingGlobal(false); 85 | } 86 | } 87 | }; 88 | 89 | const toggleTable = (key: string) => { 90 | setExpandedTables((prevState) => ({ 91 | ...prevState, 92 | [key]: !prevState[key], 93 | })); 94 | }; 95 | 96 | return ( 97 | 103 | 106 | {/* Close Button at the top-right corner */} 107 | 111 | 112 | 113 | 114 | {/* First Row: TextField */} 115 | 116 | setQuery(e.target.value)} 119 | // onKeyDown={async (e) => { 120 | // if (e.key === "Enter" && !loadingLocal) { 121 | // await handleSearch("local"); // Default to global search on enter 122 | // } 123 | // }} 124 | placeholder="Enter search query for API" 125 | fullWidth 126 | margin="normal" 127 | /> 128 | 129 | {/* Second Row: Buttons */} 130 | 131 | 144 | 158 | 159 | 160 | {!serverUp && ( 161 | 162 | Server is not running. Please start the server to use the API. 163 | Follow the instructions at{" "} 164 | 169 | graphrag-api 170 | 171 | . 172 | 173 | )} 174 | {!localSearchEnabled && ( 175 | 176 | Please enable "Include Text Unit" and "Include Communities" 177 | {hasCovariates && ', and "Include Covariates"'} to use Local 178 | Search. 179 | 180 | )} 181 | {!globalSearchEnabled && ( 182 | 183 | Please enable "Include Communities" to use Global Search. 184 | 185 | )} 186 | 187 | 188 | {apiSearchResults && ( 189 | <> 190 | {/* Search Results Card */} 191 | 192 | 193 | 194 | 195 | {apiSearchResults.response} 196 | 197 | 198 | 199 | 200 | {/* Metadata Card */} 201 | 202 | 203 | 204 | 205 | Completion Time:{" "} 206 | {apiSearchResults.completion_time} ms 207 | 208 | 209 | LLM Calls: {apiSearchResults.llm_calls} 210 | 211 | 212 | Prompt Tokens:{" "} 213 | {apiSearchResults.prompt_tokens} 214 | 215 | 216 | 217 | 218 | {/* Context Data Tables */} 219 | {apiSearchResults && 220 | apiSearchResults.context_data && 221 | Object.entries(apiSearchResults.context_data).map( 222 | ([key, data], index) => ( 223 | 224 | toggleTable(key)}> 228 | {expandedTables[key] ? ( 229 | 230 | ) : ( 231 | 232 | )} 233 | 234 | } 235 | /> 236 | 241 | 242 | {Array.isArray(data) && data.length > 0 ? ( 243 | 244 | 245 | 246 | 247 | {Object.keys(data[0]).map( 248 | (columnName, idx) => ( 249 | 250 | {columnName.charAt(0).toUpperCase() + 251 | columnName.slice(1)} 252 | 253 | ) 254 | )} 255 | 256 | 257 | 258 | {data.map((row, rowIndex) => ( 259 | 260 | {Object.values(row).map( 261 | (value, cellIndex) => ( 262 | 263 | {typeof value === "string" 264 | ? value 265 | : JSON.stringify(value, null, 2)} 266 | 267 | ) 268 | )} 269 | 270 | ))} 271 | 272 |
273 |
274 | ) : ( 275 | 276 | No data available 277 | 278 | )} 279 |
280 |
281 |
282 | ) 283 | )} 284 | 285 | )} 286 |
287 |
288 | ); 289 | }; 290 | 291 | export default APISearchDrawer; 292 | -------------------------------------------------------------------------------- /src/app/components/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | MaterialReactTable, 4 | useMaterialReactTable, 5 | MRT_ColumnDef, 6 | } from "material-react-table"; 7 | import { Box } from "@mui/material"; 8 | 9 | interface DataTableProps { 10 | data: T[]; 11 | columns: MRT_ColumnDef[]; 12 | } 13 | 14 | const DataTable = ({ 15 | data, 16 | columns, 17 | }: DataTableProps): React.ReactElement => { 18 | const table = useMaterialReactTable({ 19 | data, 20 | columns, 21 | initialState: { 22 | columnVisibility: { 23 | graph_embedding: false, 24 | description_embedding: false, 25 | // text_unit_ids: false, 26 | // relationship_ids: false, 27 | }, 28 | density: "compact", 29 | }, 30 | }); 31 | 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default DataTable; 40 | -------------------------------------------------------------------------------- /src/app/components/DataTableContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Typography, 4 | Box, 5 | Drawer, 6 | List, 7 | ListItemButton, 8 | ListItemText, 9 | } from "@mui/material"; 10 | import DataTable from "./DataTable"; 11 | import { Entity, entityColumns } from "../models/entity"; 12 | import { Relationship, relationshipColumns } from "../models/relationship"; 13 | import { Document, documentColumns } from "../models/document"; 14 | import { TextUnit, textUnitColumns } from "../models/text-unit"; 15 | import { Community, communityColumns } from "../models/community"; 16 | import { 17 | CommunityReport, 18 | communityReportColumns, 19 | } from "../models/community-report"; 20 | import { Covariate, covariateColumns } from "../models/covariate"; 21 | 22 | interface DataTableContainerProps { 23 | selectedTable: string; 24 | setSelectedTable: ( 25 | value: React.SetStateAction< 26 | | "entities" 27 | | "relationships" 28 | | "documents" 29 | | "textunits" 30 | | "communities" 31 | | "communityReports" 32 | | "covariates" 33 | > 34 | ) => void; 35 | entities: Entity[]; 36 | relationships: Relationship[]; 37 | documents: Document[]; 38 | textunits: TextUnit[]; 39 | communities: Community[]; 40 | communityReports: CommunityReport[]; 41 | covariates: Covariate[]; 42 | } 43 | 44 | const DataTableContainer: React.FC = ({ 45 | selectedTable, 46 | setSelectedTable, 47 | entities, 48 | relationships, 49 | documents, 50 | textunits, 51 | communities, 52 | communityReports, 53 | covariates, 54 | }) => { 55 | return ( 56 | <> 57 | 66 | 67 | setSelectedTable("entities")} 70 | > 71 | 72 | 73 | setSelectedTable("relationships")} 76 | > 77 | 78 | 79 | setSelectedTable("documents")} 82 | > 83 | 84 | 85 | setSelectedTable("textunits")} 88 | > 89 | 90 | 91 | setSelectedTable("communities")} 94 | > 95 | 96 | 97 | 98 | setSelectedTable("communityReports")} 101 | > 102 | 103 | 104 | 105 | setSelectedTable("covariates")} 108 | > 109 | 110 | 111 | 112 | 113 | 114 | {selectedTable === "entities" && ( 115 | <> 116 | 117 | Entities (entities.parquet) 118 | 119 | 120 | 121 | )} 122 | {selectedTable === "relationships" && ( 123 | <> 124 | 125 | Relationships (relationships.parquet) 126 | 127 | 128 | 129 | )} 130 | {selectedTable === "documents" && ( 131 | <> 132 | 133 | Documents (documents.parquet) 134 | 135 | 136 | 137 | )} 138 | {selectedTable === "textunits" && ( 139 | <> 140 | 141 | TextUnits (text_units.parquet) 142 | 143 | 144 | 145 | )} 146 | {selectedTable === "communities" && ( 147 | <> 148 | 149 | Communities (communities.parquet) 150 | 151 | 152 | 153 | )} 154 | {selectedTable === "communityReports" && ( 155 | <> 156 | 157 | Community Reports (community_reports.parquet) 158 | 159 | 163 | 164 | )} 165 | {selectedTable === "covariates" && ( 166 | <> 167 | 168 | Covariates (covariates.parquet) 169 | 170 | 171 | 172 | )} 173 | 174 | 175 | ); 176 | }; 177 | 178 | export default DataTableContainer; 179 | -------------------------------------------------------------------------------- /src/app/components/DetailDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Card, 5 | CardContent, 6 | Chip, 7 | Drawer, 8 | IconButton, 9 | Typography, 10 | } from "@mui/material"; 11 | import CloseIcon from "@mui/icons-material/Close"; 12 | import DataTable from "./DataTable"; 13 | import { 14 | CustomLink, 15 | CustomNode, 16 | customLinkColumns, 17 | customNodeColumns, 18 | } from "../models/custom-graph-data"; 19 | import { textUnitColumns } from "../models/text-unit"; 20 | import { communityColumns } from "../models/community"; 21 | import { 22 | communityReportColumns, 23 | findingColumns, 24 | } from "../models/community-report"; 25 | import { documentColumns } from "../models/document"; 26 | import { covariateColumns } from "../models/covariate"; 27 | import { MRT_ColumnDef } from "material-react-table"; 28 | import { entityColumns } from "../models/entity"; 29 | 30 | interface DetailDrawerProps { 31 | bottomDrawerOpen: boolean; 32 | setBottomDrawerOpen: React.Dispatch>; 33 | selectedNode: CustomNode | null; 34 | selectedRelationship: CustomLink | null; 35 | linkedNodes: CustomNode[]; 36 | linkedRelationships: CustomLink[]; 37 | } 38 | 39 | const DetailDrawer: React.FC = ({ 40 | bottomDrawerOpen, 41 | setBottomDrawerOpen, 42 | selectedNode, 43 | selectedRelationship, 44 | linkedNodes, 45 | linkedRelationships, 46 | }) => { 47 | const getNodeName = (node: string | CustomNode) => { 48 | return typeof node === "object" ? node.name : node; 49 | }; 50 | 51 | const getNodeType = (node: string | CustomNode) => { 52 | return typeof node === "object" ? node.type : node; 53 | }; 54 | 55 | const getFilteredNodeColumns = ( 56 | types: string[] 57 | ): MRT_ColumnDef[] => { 58 | const validAccessorKeys = new Set(); 59 | if (types.includes("CHUNK")) { 60 | textUnitColumns.forEach((tc) => { 61 | if (tc.accessorKey) { 62 | validAccessorKeys.add(tc.accessorKey); 63 | } 64 | }); 65 | } 66 | 67 | if (types.includes("COMMUNITY")) { 68 | communityColumns.forEach((tc) => { 69 | if (tc.accessorKey) { 70 | validAccessorKeys.add(tc.accessorKey); 71 | } 72 | }); 73 | communityReportColumns.forEach((tc) => { 74 | if (tc.accessorKey) { 75 | validAccessorKeys.add(tc.accessorKey); 76 | } 77 | }); 78 | } 79 | 80 | if (types.includes("RAW_DOCUMENT")) { 81 | documentColumns.forEach((tc) => { 82 | if (tc.accessorKey) { 83 | validAccessorKeys.add(tc.accessorKey); 84 | } 85 | }); 86 | } 87 | 88 | if (types.includes("COVARIATE")) { 89 | covariateColumns.forEach((tc) => { 90 | if (tc.accessorKey) { 91 | validAccessorKeys.add(tc.accessorKey); 92 | } 93 | }); 94 | } 95 | 96 | if (types.includes("FINDING")) { 97 | findingColumns.forEach((tc) => { 98 | if (tc.accessorKey) { 99 | validAccessorKeys.add(tc.accessorKey); 100 | } 101 | }); 102 | } 103 | 104 | entityColumns.forEach((tc) => { 105 | if (tc.accessorKey) { 106 | validAccessorKeys.add(tc.accessorKey); 107 | } 108 | }); 109 | 110 | validAccessorKeys.add("uuid"); 111 | return customNodeColumns.filter( 112 | (column) => 113 | column.accessorKey && validAccessorKeys.has(column.accessorKey) 114 | ); 115 | }; 116 | 117 | const linkedNodeTypes = [...new Set(linkedNodes.map((node) => node.type))]; 118 | const filteredColumns = getFilteredNodeColumns(linkedNodeTypes); 119 | 120 | return ( 121 | setBottomDrawerOpen(false)} 125 | sx={{ zIndex: 1500 }} 126 | > 127 | 128 | 136 | {selectedNode ? ( 137 | 138 | {/* Node Details: {selectedNode.id.toString()} */} 139 | Node Details: {selectedNode.name.toString()} 140 | 141 | ) : ( 142 | 143 | {" "} 144 | {selectedRelationship && ( 145 | <> 146 | {"(:"} 147 | {getNodeType(selectedRelationship.source)} {"{name: "} 148 | {"'"} 149 | {getNodeName(selectedRelationship.source)} 150 | {"'"} 151 | {"}"} 152 | {")"} 153 | {"-[:"} 154 | {selectedRelationship.type} 155 | {"]->"} 156 | {"(:"} 157 | {getNodeType(selectedRelationship.target)} {"{name: "} 158 | {"'"} 159 | {getNodeName(selectedRelationship.target)} 160 | {"'"} 161 | {"}"} 162 | {")"} 163 | 164 | )} 165 | 166 | )} 167 | setBottomDrawerOpen(false)} 169 | sx={{ marginLeft: "auto" }} 170 | > 171 | 172 | 173 | 174 | {selectedNode && ( 175 | 176 | 177 | 178 | Node Information 179 | 180 | ID: {selectedNode.uuid} 181 | Title: {selectedNode.name} 182 | {selectedNode.covariate_type && ( 183 | 184 | Covariate Type: {selectedNode.covariate_type} 185 | 186 | )} 187 | 188 | Type: {" "} 189 | 190 | {selectedNode.title && ( 191 | 192 | Community Report Title: {selectedNode.title} 193 | 194 | )} 195 | {selectedNode.summary && ( 196 | Summary: {selectedNode.summary} 197 | )} 198 | {selectedNode.n_tokens && ( 199 | 200 | Number of Tokens: {selectedNode.n_tokens} 201 | 202 | )} 203 | 204 | {selectedNode.description && ( 205 | Description: {selectedNode.description} 206 | )} 207 | {selectedNode.human_readable_id && ( 208 | 209 | Human Readable ID: {selectedNode.human_readable_id} 210 | 211 | )} 212 | 213 | {/* {selectedNode.human_readable_id || 214 | (selectedNode.human_readable_id === 0 && ( 215 | 216 | Human Readable ID: {selectedNode.human_readable_id} 217 | 218 | ))} */} 219 | {selectedNode.raw_content && ( 220 | Raw Content: {selectedNode.raw_content} 221 | )} 222 | 223 | 224 | )} 225 | {selectedRelationship && ( 226 | 227 | 228 | 229 | Relationship Information: 230 | 231 | ID: {selectedRelationship.id} 232 | 233 | 234 | Source: {getNodeName(selectedRelationship.source)} 235 | 236 | 237 | Target: {getNodeName(selectedRelationship.target)} 238 | 239 | Type: {selectedRelationship.type} 240 | {selectedRelationship.description && ( 241 | 242 | Description: {selectedRelationship.description} 243 | 244 | )} 245 | {selectedRelationship.human_readable_id && ( 246 | 247 | Human Readable ID: {selectedRelationship.human_readable_id} 248 | 249 | )} 250 | {selectedRelationship.weight && ( 251 | Weight: {selectedRelationship.weight} 252 | )} 253 | {selectedRelationship.source_degree && ( 254 | 255 | Source Degree: {selectedRelationship.source_degree} 256 | 257 | )} 258 | {selectedRelationship.target_degree && ( 259 | 260 | Target Degree: {selectedRelationship.target_degree} 261 | 262 | )} 263 | {selectedRelationship.rank && ( 264 | Rank: {selectedRelationship.rank} 265 | )} 266 | 267 | 268 | )} 269 | 270 | 271 | Linked Nodes 272 | 273 | 274 | 275 | {selectedNode && ( 276 | 277 | 278 | Linked Relationships 279 | 280 | 281 | ({ 284 | ...link, 285 | source: getNodeName(link.source), 286 | target: getNodeName(link.target), 287 | }))} 288 | /> 289 | 290 | )} 291 | 292 | 293 | ); 294 | }; 295 | 296 | export default DetailDrawer; 297 | -------------------------------------------------------------------------------- /src/app/components/DropZone.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Typography } from "@mui/material"; 3 | import { useTheme } from "@mui/material/styles"; 4 | 5 | interface DropZoneProps { 6 | getRootProps: () => any; 7 | getInputProps: () => any; 8 | isDragActive: boolean; 9 | } 10 | 11 | const DropZone: React.FC = ({ 12 | getRootProps, 13 | getInputProps, 14 | isDragActive, 15 | }) => { 16 | const theme = useTheme(); 17 | 18 | return ( 19 | 39 | 40 | {isDragActive ? ( 41 | Drop the files here... 42 | ) : ( 43 | 44 | Drag 'n' drop parquet files here, or click to select files 45 | 46 | )} 47 | 48 | ); 49 | }; 50 | 51 | export default DropZone; 52 | -------------------------------------------------------------------------------- /src/app/components/GraphDataHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import GraphViewer from "./GraphViewer"; 4 | import { Box, Container, Tab, Tabs } from "@mui/material"; 5 | import { useDropzone } from "react-dropzone"; 6 | import DropZone from "./DropZone"; 7 | import Introduction from "./Introduction"; 8 | import useFileHandler from "../hooks/useFileHandler"; 9 | import useGraphData from "../hooks/useGraphData"; 10 | import DataTableContainer from "./DataTableContainer"; 11 | import ReactGA from "react-ga4"; 12 | 13 | const GraphDataHandler: React.FC = () => { 14 | const location = useLocation(); 15 | const navigate = useNavigate(); 16 | 17 | const [tabIndex, setTabIndex] = useState(0); 18 | const [graphType, setGraphType] = useState<"2d" | "3d">("2d"); 19 | const [isFullscreen, setIsFullscreen] = useState(false); 20 | const [selectedTable, setSelectedTable] = useState< 21 | | "entities" 22 | | "relationships" 23 | | "documents" 24 | | "textunits" 25 | | "communities" 26 | | "communityReports" 27 | | "covariates" 28 | >("entities"); 29 | const [includeDocuments, setIncludeDocuments] = useState(false); 30 | const [includeTextUnits, setIncludeTextUnits] = useState(false); 31 | const [includeCommunities, setIncludeCommunities] = useState(false); 32 | const [includeCovariates, setIncludeCovariates] = useState(false); 33 | 34 | const { 35 | entities, 36 | relationships, 37 | documents, 38 | textunits, 39 | communities, 40 | covariates, 41 | communityReports, 42 | handleFilesRead, 43 | loadDefaultFiles, 44 | } = useFileHandler(); 45 | 46 | const graphData = useGraphData( 47 | entities, 48 | relationships, 49 | documents, 50 | textunits, 51 | communities, 52 | communityReports, 53 | covariates, 54 | includeDocuments, 55 | includeTextUnits, 56 | includeCommunities, 57 | includeCovariates 58 | ); 59 | 60 | const hasDocuments = documents.length > 0; 61 | const hasTextUnits = textunits.length > 0; 62 | const hasCommunities = communities.length > 0; 63 | const hasCovariates = covariates.length > 0; 64 | 65 | useEffect(() => { 66 | if (process.env.NODE_ENV === "development") { 67 | loadDefaultFiles(); 68 | } 69 | // eslint-disable-next-line 70 | }, []); 71 | 72 | useEffect(() => { 73 | const measurementId = process.env.REACT_APP_GA_MEASUREMENT_ID; 74 | if (measurementId) { 75 | ReactGA.initialize(measurementId); 76 | } else { 77 | console.error("Google Analytics measurement ID not found"); 78 | } 79 | }, []); 80 | 81 | useEffect(() => { 82 | // **Set tab index based on the current path** 83 | switch (location.pathname) { 84 | case "/upload": 85 | setTabIndex(0); 86 | break; 87 | case "/graph": 88 | setTabIndex(1); 89 | break; 90 | case "/data": 91 | setTabIndex(2); 92 | break; 93 | default: 94 | setTabIndex(0); 95 | } 96 | }, [location.pathname]); 97 | 98 | const onDrop = (acceptedFiles: File[]) => { 99 | handleFilesRead(acceptedFiles); 100 | navigate("/graph", { replace: true }); 101 | }; 102 | 103 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 104 | onDrop, 105 | noClick: false, 106 | noKeyboard: true, 107 | accept: { 108 | "application/x-parquet": [".parquet"], 109 | }, 110 | }); 111 | 112 | const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { 113 | setTabIndex(newValue); 114 | let path = "/upload"; 115 | if (newValue === 1) path = "/graph"; 116 | if (newValue === 2) path = "/data"; 117 | navigate(path); 118 | ReactGA.send({ 119 | hitType: "event", 120 | eventCategory: "Tabs", 121 | eventAction: "click", 122 | eventLabel: `Tab ${newValue}`, 123 | }); 124 | }; 125 | 126 | const toggleGraphType = () => { 127 | setGraphType((prevType) => (prevType === "2d" ? "3d" : "2d")); 128 | }; 129 | 130 | const toggleFullscreen = () => { 131 | setIsFullscreen(!isFullscreen); 132 | }; 133 | 134 | return ( 135 | <> 136 | 137 | 138 | 139 | 140 | 141 | {tabIndex === 0 && ( 142 | 150 | 151 | 152 | 153 | )} 154 | {tabIndex === 1 && ( 155 | 167 | 176 | setIncludeDocuments(!includeDocuments) 177 | } 178 | onIncludeTextUnitsChange={() => 179 | setIncludeTextUnits(!includeTextUnits) 180 | } 181 | includeCommunities={includeCommunities} 182 | onIncludeCommunitiesChange={() => 183 | setIncludeCommunities(!includeCommunities) 184 | } 185 | includeCovariates={includeCovariates} 186 | onIncludeCovariatesChange={() => 187 | setIncludeCovariates(!includeCovariates) 188 | } 189 | hasDocuments={hasDocuments} 190 | hasTextUnits={hasTextUnits} 191 | hasCommunities={hasCommunities} 192 | hasCovariates={hasCovariates} 193 | /> 194 | 195 | )} 196 | 197 | {tabIndex === 2 && ( 198 | 199 | 210 | 211 | )} 212 | 213 | ); 214 | }; 215 | 216 | export default GraphDataHandler; 217 | -------------------------------------------------------------------------------- /src/app/components/GraphViewer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useRef, useEffect } from "react"; 2 | import ForceGraph2D from "react-force-graph-2d"; 3 | import ForceGraph3D from "react-force-graph-3d"; 4 | import { 5 | CustomGraphData, 6 | CustomLink, 7 | CustomNode, 8 | } from "../models/custom-graph-data"; 9 | import { 10 | Box, 11 | Button, 12 | Checkbox, 13 | FormControlLabel, 14 | FormGroup, 15 | IconButton, 16 | Switch, 17 | Tooltip, 18 | Typography, 19 | useTheme, 20 | } from "@mui/material"; 21 | import FullscreenIcon from "@mui/icons-material/Fullscreen"; 22 | import FullscreenExitIcon from "@mui/icons-material/FullscreenExit"; 23 | import SearchIcon from "@mui/icons-material/Search"; 24 | import DeleteIcon from "@mui/icons-material/Delete"; 25 | import Fuse from "fuse.js"; 26 | import { 27 | CSS2DRenderer, 28 | CSS2DObject, 29 | } from "three/examples/jsm/renderers/CSS2DRenderer"; 30 | import * as THREE from "three"; 31 | import { Renderer } from "three"; 32 | import SearchDrawer from "./SearchDrawer"; 33 | import DetailDrawer from "./DetailDrawer"; 34 | import { SearchResult } from "../models/search-result"; 35 | import agent from "../api/agent"; 36 | import APISearchDrawer from "./APISearchDrawer"; 37 | import SpriteText from "three-spritetext"; 38 | 39 | type Coords = { 40 | x: number; 41 | y: number; 42 | z: number; 43 | }; 44 | 45 | interface GraphViewerProps { 46 | data: CustomGraphData; 47 | graphType: "2d" | "3d"; 48 | isFullscreen: boolean; 49 | onToggleFullscreen: () => void; 50 | onToggleGraphType: (event: React.ChangeEvent) => void; 51 | includeDocuments: boolean; 52 | onIncludeDocumentsChange: React.Dispatch>; 53 | includeTextUnits: boolean; 54 | onIncludeTextUnitsChange: React.Dispatch>; 55 | includeCommunities: boolean; 56 | onIncludeCommunitiesChange: React.Dispatch>; 57 | includeCovariates: boolean; 58 | onIncludeCovariatesChange: React.Dispatch>; 59 | hasDocuments: boolean; 60 | hasTextUnits: boolean; 61 | hasCommunities: boolean; 62 | hasCovariates: boolean; 63 | } 64 | 65 | const NODE_R = 8; 66 | 67 | const GraphViewer: React.FC = ({ 68 | data, 69 | graphType, 70 | isFullscreen, 71 | includeDocuments, 72 | onIncludeDocumentsChange, 73 | includeTextUnits, 74 | onIncludeTextUnitsChange, 75 | includeCommunities, 76 | onIncludeCommunitiesChange, 77 | includeCovariates, 78 | onIncludeCovariatesChange, 79 | onToggleFullscreen, 80 | onToggleGraphType, 81 | hasDocuments, 82 | hasTextUnits, 83 | hasCommunities, 84 | hasCovariates, 85 | }) => { 86 | const theme = useTheme(); 87 | const [highlightNodes, setHighlightNodes] = useState>( 88 | new Set() 89 | ); 90 | const [highlightLinks, setHighlightLinks] = useState>( 91 | new Set() 92 | ); 93 | const [hoverNode, setHoverNode] = useState(null); 94 | const [searchTerm, setSearchTerm] = useState(""); 95 | const [searchResults, setSearchResults] = useState< 96 | (CustomNode | CustomLink)[] 97 | >([]); 98 | const [rightDrawerOpen, setRightDrawerOpen] = useState(false); 99 | const [bottomDrawerOpen, setBottomDrawerOpen] = useState(false); 100 | const [selectedNode, setSelectedNode] = useState(null); 101 | const [selectedRelationship, setSelectedRelationship] = 102 | useState(null); 103 | const [linkedNodes, setLinkedNodes] = useState([]); 104 | const [linkedRelationships, setLinkedRelationships] = useState( 105 | [] 106 | ); 107 | const [showLabels, setShowLabels] = useState(false); 108 | const [showLinkLabels, setShowLinkLabels] = useState(false); 109 | const [showHighlight, setShowHighlight] = useState(true); 110 | const graphRef = useRef(); 111 | const extraRenderers = [new CSS2DRenderer() as any as Renderer]; 112 | const nodeCount = data.nodes.length; 113 | const linkCount = data.links.length; 114 | 115 | const [apiDrawerOpen, setApiDrawerOpen] = useState(false); 116 | const [apiSearchResults, setApiSearchResults] = useState( 117 | null 118 | ); 119 | const [serverUp, setServerUp] = useState(false); 120 | 121 | const [graphData, setGraphData] = useState(data); 122 | 123 | const initialGraphData = useRef(data); 124 | 125 | useEffect(() => { 126 | setGraphData(data); 127 | initialGraphData.current = data; 128 | }, [data]); 129 | 130 | useEffect(() => { 131 | checkServerStatus(); 132 | }, []); 133 | 134 | const toggleApiDrawer = (open: boolean) => () => { 135 | setApiDrawerOpen(open); 136 | }; 137 | 138 | const handleApiSearch = async ( 139 | query: string, 140 | searchType: "local" | "global" 141 | ) => { 142 | try { 143 | const data: SearchResult = 144 | searchType === "local" 145 | ? await agent.Search.local(query) 146 | : await agent.Search.global(query); 147 | 148 | setApiSearchResults(data); 149 | // Process the search result to update the graph data 150 | updateGraphData(data.context_data); 151 | } catch (err) { 152 | console.error("An error occurred during the API search.", err); 153 | } finally { 154 | } 155 | }; 156 | 157 | const checkServerStatus = async () => { 158 | try { 159 | const response = await agent.Status.check(); 160 | if (response.status === "Server is up and running") { 161 | setServerUp(true); 162 | } else { 163 | setServerUp(false); 164 | } 165 | } catch (error) { 166 | setServerUp(false); 167 | } 168 | }; 169 | 170 | const updateGraphData = (contextData: any) => { 171 | if (!contextData) return; 172 | 173 | const newNodes: CustomNode[] = []; 174 | const newLinks: CustomLink[] = []; 175 | 176 | const baseGraphData = initialGraphData.current; 177 | 178 | // Assuming contextData has keys like entities, reports, relationships, sources 179 | Object.entries(contextData).forEach(([key, items]) => { 180 | if (Array.isArray(items)) { 181 | items.forEach((item) => { 182 | if (key === "relationships") { 183 | // Handle links 184 | const existingLink = baseGraphData.links.find( 185 | (link) => 186 | link.human_readable_id?.toString() === item.id.toString() 187 | ); 188 | 189 | if (existingLink) { 190 | newLinks.push(existingLink); 191 | } 192 | } else if (key === "entities") { 193 | const existingNode = baseGraphData.nodes.find( 194 | (node) => 195 | node.human_readable_id?.toString() === item.id.toString() && 196 | !node.covariate_type 197 | ); 198 | if (existingNode) { 199 | newNodes.push(existingNode); 200 | } 201 | } else if (key === "reports") { 202 | const existingNode = baseGraphData.nodes.find( 203 | (node) => node.uuid === item.id.toString() 204 | ); 205 | if (existingNode) { 206 | newNodes.push(existingNode); 207 | } 208 | } else if (key === "sources") { 209 | const existingNode = baseGraphData.nodes.find( 210 | (node) => node.text?.toString() === item.text 211 | ); 212 | if (existingNode) { 213 | newNodes.push(existingNode); 214 | } 215 | } else if (key === "covariates" || key === "claims") { 216 | const existingNode = baseGraphData.nodes.find( 217 | (node) => 218 | node.human_readable_id?.toString() === item.id.toString() && 219 | node.covariate_type 220 | ); 221 | if (existingNode) { 222 | newNodes.push(existingNode); 223 | } 224 | } 225 | }); 226 | } 227 | }); 228 | 229 | // Update the graph data with the new nodes and links 230 | const updatedGraphData: CustomGraphData = { 231 | nodes: [...newNodes], 232 | links: [...newLinks], 233 | }; 234 | 235 | // Set the updated data to trigger re-render 236 | setGraphData(updatedGraphData); 237 | }; 238 | 239 | const fuse = new Fuse([...data.nodes, ...data.links], { 240 | keys: [ 241 | "uuid", 242 | "id", 243 | "name", 244 | "type", 245 | "description", 246 | "source", 247 | "target", 248 | "title", 249 | "summary", 250 | ], 251 | threshold: 0.3, 252 | }); 253 | 254 | const handleNodeHover = useCallback((node: CustomNode | null) => { 255 | const newHighlightNodes = new Set(); 256 | const newHighlightLinks = new Set(); 257 | 258 | if (node) { 259 | newHighlightNodes.add(node); 260 | node.neighbors?.forEach((neighbor) => newHighlightNodes.add(neighbor)); 261 | node.links?.forEach((link) => newHighlightLinks.add(link)); 262 | } 263 | 264 | setHighlightNodes(newHighlightNodes); 265 | setHighlightLinks(newHighlightLinks); 266 | setHoverNode(node); 267 | }, []); 268 | 269 | const handleLinkHover = useCallback((link: CustomLink | null) => { 270 | const newHighlightNodes = new Set(); 271 | const newHighlightLinks = new Set(); 272 | 273 | if (link) { 274 | newHighlightLinks.add(link); 275 | if (typeof link.source !== "string") newHighlightNodes.add(link.source); 276 | if (typeof link.target !== "string") newHighlightNodes.add(link.target); 277 | } 278 | 279 | setHighlightNodes(newHighlightNodes); 280 | setHighlightLinks(newHighlightLinks); 281 | }, []); 282 | 283 | const paintRing = useCallback( 284 | (node: CustomNode, ctx: CanvasRenderingContext2D) => { 285 | ctx.beginPath(); 286 | ctx.arc(node.x!, node.y!, NODE_R * 1.4, 0, 2 * Math.PI, false); 287 | if (highlightNodes.has(node)) { 288 | ctx.fillStyle = node === hoverNode ? "red" : "orange"; 289 | ctx.globalAlpha = 1; // full opacity 290 | } else { 291 | ctx.fillStyle = "gray"; 292 | ctx.globalAlpha = 0.3; // reduced opacity for non-highlighted nodes 293 | } 294 | ctx.fill(); 295 | ctx.globalAlpha = 1; // reset alpha for other drawings 296 | }, 297 | [hoverNode, highlightNodes] 298 | ); 299 | 300 | const handleSearch = () => { 301 | const results = fuse.search(searchTerm).map((result) => result.item); 302 | const nodeResults = results.filter((item) => "neighbors" in item); 303 | const linkResults = results.filter( 304 | (item) => "source" in item && "target" in item 305 | ); 306 | setSearchResults([...nodeResults, ...linkResults]); 307 | setRightDrawerOpen(true); 308 | }; 309 | 310 | const toggleDrawer = (open: boolean) => () => { 311 | setRightDrawerOpen(open); 312 | }; 313 | 314 | const handleFocusButtonClick = (node: CustomNode) => { 315 | const newHighlightNodes = new Set(); 316 | newHighlightNodes.add(node); 317 | node.neighbors?.forEach((neighbor) => newHighlightNodes.add(neighbor)); 318 | node.links?.forEach((link) => highlightLinks.add(link)); 319 | 320 | setHighlightNodes(newHighlightNodes); 321 | setHoverNode(node); 322 | 323 | if (graphRef.current) { 324 | if (graphType === "2d") { 325 | graphRef.current.centerAt(node.x, node.y, 1000); 326 | graphRef.current.zoom(8, 1000); 327 | } else { 328 | graphRef.current.cameraPosition( 329 | { x: node.x, y: node.y, z: 300 }, // new position 330 | { x: node.x, y: node.y, z: 0 }, // lookAt 331 | 3000 // ms transition duration 332 | ); 333 | } 334 | } 335 | 336 | // Simulate mouse hover on the focused node 337 | setTimeout(() => { 338 | handleNodeHover(node); 339 | }, 1000); // Adjust delay as needed 340 | 341 | setRightDrawerOpen(false); 342 | }; 343 | 344 | const handleFocusLinkClick = (link: CustomLink) => { 345 | const newHighlightNodes = new Set(); 346 | const newHighlightLinks = new Set(); 347 | 348 | newHighlightLinks.add(link); 349 | let sourceNode: CustomNode | undefined; 350 | let targetNode: CustomNode | undefined; 351 | 352 | if (typeof link.source !== "string") { 353 | newHighlightNodes.add(link.source); 354 | sourceNode = link.source; 355 | } 356 | 357 | if (typeof link.target !== "string") { 358 | newHighlightNodes.add(link.target); 359 | targetNode = link.target; 360 | } 361 | 362 | setHighlightNodes(newHighlightNodes); 363 | setHighlightLinks(newHighlightLinks); 364 | 365 | if ( 366 | graphRef.current && 367 | sourceNode && 368 | targetNode && 369 | sourceNode.x && 370 | targetNode.x && 371 | sourceNode.y && 372 | targetNode.y 373 | ) { 374 | const midX = (sourceNode.x + targetNode.x) / 2; 375 | const midY = (sourceNode.y + targetNode.y) / 2; 376 | 377 | if (graphType === "2d") { 378 | graphRef.current.centerAt(midX, midY, 1000); 379 | graphRef.current.zoom(8, 1000); 380 | } else { 381 | graphRef.current.cameraPosition( 382 | { x: midX, y: midY, z: 300 }, // new position 383 | { x: midX, y: midY, z: 0 }, // lookAt 384 | 3000 // ms transition duration 385 | ); 386 | } 387 | } 388 | 389 | // Simulate mouse hover on the focused link 390 | setTimeout(() => { 391 | handleLinkHover(link); 392 | }, 1000); // Adjust delay as needed 393 | 394 | setRightDrawerOpen(false); 395 | }; 396 | 397 | const handleNodeClick = (node: CustomNode) => { 398 | setSelectedRelationship(null); 399 | setSelectedNode(node); 400 | setLinkedNodes(node.neighbors || []); 401 | setLinkedRelationships(node.links || []); 402 | setBottomDrawerOpen(true); 403 | }; 404 | 405 | const handleLinkClick = (link: CustomLink) => { 406 | setSelectedNode(null); 407 | setSelectedRelationship(link); 408 | const linkSource = 409 | typeof link.source === "object" 410 | ? (link.source as CustomNode).id 411 | : link.source; 412 | const linkTarget = 413 | typeof link.target === "object" 414 | ? (link.target as CustomNode).id 415 | : link.target; 416 | const sourceNode = data.nodes.find((node) => node.id === linkSource); 417 | const targetNode = data.nodes.find((node) => node.id === linkTarget); 418 | if (sourceNode && targetNode) { 419 | const linkedNodes = [sourceNode, targetNode]; 420 | setLinkedNodes(linkedNodes); 421 | const linkedRelationships = [link]; 422 | setLinkedRelationships(linkedRelationships); 423 | setBottomDrawerOpen(true); 424 | } 425 | }; 426 | 427 | const getBackgroundColor = () => 428 | theme.palette.mode === "dark" ? "#000000" : "#FFFFFF"; 429 | 430 | const getLinkColor = (link: CustomLink) => 431 | theme.palette.mode === "dark" ? "gray" : "lightgray"; 432 | 433 | const get3DLinkColor = (link: CustomLink) => 434 | theme.palette.mode === "dark" ? "lightgray" : "gray"; 435 | 436 | const getlinkDirectionalParticleColor = (link: CustomLink) => 437 | theme.palette.mode === "dark" ? "lightgray" : "gray"; 438 | 439 | const renderNodeLabel = (node: CustomNode, ctx: CanvasRenderingContext2D) => { 440 | if (!showLabels) return; // Only render the label if showLabels is true 441 | 442 | const label = node.name || ""; 443 | const fontSize = 4; 444 | const padding = 2; 445 | ctx.font = `${fontSize}px Sans-Serif`; 446 | 447 | // Set the styles based on the theme mode 448 | const backgroundColor = 449 | theme.palette.mode === "dark" 450 | ? "rgba(0, 0, 0, 0.6)" 451 | : "rgba(255, 255, 255, 0.6)"; 452 | 453 | // Calculate label dimensions 454 | const textWidth = ctx.measureText(label).width; 455 | const boxWidth = textWidth + padding * 2; 456 | const boxHeight = fontSize + padding * 2; 457 | 458 | if (node.x && node.y) { 459 | // Draw the background rectangle with rounded corners 460 | ctx.fillStyle = backgroundColor; 461 | ctx.beginPath(); 462 | ctx.moveTo(node.x - boxWidth / 2 + 5, node.y - boxHeight / 2); 463 | ctx.lineTo(node.x + boxWidth / 2 - 5, node.y - boxHeight / 2); 464 | ctx.quadraticCurveTo( 465 | node.x + boxWidth / 2, 466 | node.y - boxHeight / 2, 467 | node.x + boxWidth / 2, 468 | node.y - boxHeight / 2 + 5 469 | ); 470 | ctx.lineTo(node.x + boxWidth / 2, node.y + boxHeight / 2 - 5); 471 | ctx.quadraticCurveTo( 472 | node.x + boxWidth / 2, 473 | node.y + boxHeight / 2, 474 | node.x + boxWidth / 2 - 5, 475 | node.y + boxHeight / 2 476 | ); 477 | ctx.lineTo(node.x - boxWidth / 2 + 5, node.y + boxHeight / 2); 478 | ctx.quadraticCurveTo( 479 | node.x - boxWidth / 2, 480 | node.y + boxHeight / 2, 481 | node.x - boxWidth / 2, 482 | node.y + boxHeight / 2 - 5 483 | ); 484 | ctx.lineTo(node.x - boxWidth / 2, node.y - boxHeight / 2 + 5); 485 | ctx.quadraticCurveTo( 486 | node.x - boxWidth / 2, 487 | node.y - boxHeight / 2, 488 | node.x - boxWidth / 2 + 5, 489 | node.y - boxHeight / 2 490 | ); 491 | ctx.closePath(); 492 | ctx.fill(); 493 | 494 | // Draw the text in the center of the node 495 | // ctx.fillStyle = textColor; 496 | ctx.fillStyle = node.color; 497 | ctx.textAlign = "center"; 498 | ctx.textBaseline = "middle"; 499 | ctx.fillText(label, node.x, node.y); 500 | } 501 | }; 502 | 503 | const nodeThreeObject = (node: CustomNode) => { 504 | if (!showLabels) { 505 | return new THREE.Object3D(); 506 | } 507 | 508 | try { 509 | const nodeEl = document.createElement("div"); 510 | nodeEl.textContent = node.name || node.id; // Use either name or id for the label 511 | nodeEl.style.color = node.color; 512 | nodeEl.style.padding = "2px 4px"; 513 | nodeEl.style.borderRadius = "4px"; 514 | nodeEl.style.fontSize = "10px"; 515 | nodeEl.className = "node-label"; 516 | 517 | return new CSS2DObject(nodeEl); 518 | } catch (error) { 519 | console.error("Error creating 3D object:", error); 520 | return new THREE.Object3D(); // Fallback in case of error 521 | } 522 | }; 523 | 524 | const localSearchEnabled = hasCovariates 525 | ? includeTextUnits && includeCommunities && includeCovariates 526 | : includeTextUnits && includeCommunities; 527 | 528 | const clearSearchResults = () => { 529 | setGraphData(initialGraphData.current); 530 | setApiSearchResults(null); 531 | }; 532 | 533 | return ( 534 | 548 | 560 | 561 | 568 | {/* 574 | } 575 | label="3D View" 576 | /> */} 577 | {/* setShowLabels(!showLabels)} 582 | /> 583 | } 584 | label="Show Node Labels" 585 | /> 586 | setShowLinkLabels(!showLinkLabels)} 591 | /> 592 | } 593 | label="Show Relationship Labels" 594 | /> 595 | setShowHighlight(!showHighlight)} 600 | /> 601 | } 602 | label="Show Highlight" 603 | /> */} 604 | 605 | 606 | {isFullscreen ? : } 607 | 608 | 609 | 610 | 611 | 619 | 625 | } 626 | label="3D View" 627 | /> 628 | setShowLabels(!showLabels)} 633 | /> 634 | } 635 | label="Show Node Labels" 636 | /> 637 | setShowLinkLabels(!showLinkLabels)} 642 | /> 643 | } 644 | label="Show Link Labels" 645 | /> 646 | setShowHighlight(!showHighlight)} 651 | /> 652 | } 653 | label="Show Highlight" 654 | /> 655 | 656 | 657 | 658 | onIncludeDocumentsChange(!includeDocuments)} 663 | disabled={!hasDocuments || apiSearchResults !== null} 664 | /> 665 | } 666 | label="Include Documents" 667 | /> 668 | onIncludeTextUnitsChange(!includeTextUnits)} 673 | onChange={() => { 674 | if (!includeTextUnits) { 675 | onIncludeTextUnitsChange(true); 676 | } else if (includeTextUnits && !includeCovariates) { 677 | onIncludeTextUnitsChange(false); 678 | } else { 679 | onIncludeTextUnitsChange(false); 680 | onIncludeCovariatesChange(false); // Uncheck Covariates when Text Units is unchecked 681 | } 682 | }} 683 | disabled={!hasTextUnits || apiSearchResults !== null} 684 | /> 685 | } 686 | label="Include Text Units" 687 | /> 688 | onIncludeCommunitiesChange(!includeCommunities)} 693 | disabled={!hasCommunities || apiSearchResults !== null} 694 | /> 695 | } 696 | label="Include Communities" 697 | /> 698 | 699 | { 704 | if (!includeCovariates) { 705 | if (!includeTextUnits) { 706 | onIncludeTextUnitsChange(true); 707 | } 708 | onIncludeCovariatesChange(true); 709 | } else { 710 | onIncludeCovariatesChange(false); 711 | } 712 | }} 713 | disabled={!hasCovariates || apiSearchResults !== null} 714 | /> 715 | } 716 | label="Include Covariates" 717 | /> 718 | 719 | 720 | 721 | 731 | 732 | 744 | 745 | 753 | 754 | {graphType === "2d" ? ( 755 | 762 | showHighlight && highlightLinks.has(link) ? 5 : 1 763 | } 764 | linkDirectionalParticles={showHighlight ? 4 : 0} 765 | linkDirectionalParticleWidth={(link) => 766 | showHighlight && highlightLinks.has(link) ? 4 : 0 767 | } 768 | linkDirectionalParticleColor={ 769 | showHighlight ? getlinkDirectionalParticleColor : undefined 770 | } 771 | nodeCanvasObjectMode={(node) => 772 | showHighlight && highlightNodes.has(node) 773 | ? "before" 774 | : showLabels 775 | ? "after" 776 | : undefined 777 | } 778 | nodeCanvasObject={(node, ctx) => { 779 | if (showHighlight && highlightNodes.has(node)) { 780 | paintRing(node as CustomNode, ctx); 781 | } 782 | if (showLabels) { 783 | renderNodeLabel(node as CustomNode, ctx); 784 | } 785 | }} 786 | linkCanvasObjectMode={() => (showLinkLabels ? "after" : undefined)} 787 | linkCanvasObject={(link, ctx) => { 788 | if (showLinkLabels) { 789 | const label = link.type || ""; 790 | const fontSize = 4; 791 | ctx.font = `${fontSize}px Sans-Serif`; 792 | ctx.fillStyle = 793 | theme.palette.mode === "dark" ? "lightgray" : "darkgray"; 794 | const source = 795 | typeof link.source !== "string" 796 | ? (link.source as CustomNode) 797 | : null; 798 | const target = 799 | typeof link.target !== "string" 800 | ? (link.target as CustomNode) 801 | : null; 802 | 803 | if ( 804 | source && 805 | target && 806 | source.x !== undefined && 807 | target.x !== undefined && 808 | source.y !== undefined && 809 | target.y !== undefined 810 | ) { 811 | const textWidth = ctx.measureText(label).width; 812 | const posX = (source.x + target.x) / 2 - textWidth / 2; 813 | const posY = (source.y + target.y) / 2; 814 | ctx.fillText(label, posX, posY); 815 | } 816 | } 817 | }} 818 | onNodeHover={showHighlight ? handleNodeHover : undefined} 819 | onLinkHover={showHighlight ? handleLinkHover : undefined} 820 | onNodeClick={handleNodeClick} 821 | onLinkClick={handleLinkClick} 822 | backgroundColor={getBackgroundColor()} 823 | linkColor={getLinkColor} 824 | /> 825 | ) : ( 826 | 833 | showHighlight && highlightLinks.has(link) ? 5 : 1 834 | } 835 | linkDirectionalParticles={showHighlight ? 4 : 0} 836 | linkDirectionalParticleWidth={(link) => 837 | showHighlight && highlightLinks.has(link) ? 4 : 0 838 | } 839 | nodeThreeObject={nodeThreeObject} 840 | nodeThreeObjectExtend={true} 841 | onNodeHover={showHighlight ? handleNodeHover : undefined} 842 | onLinkHover={showHighlight ? handleLinkHover : undefined} 843 | onNodeClick={handleNodeClick} 844 | onLinkClick={handleLinkClick} 845 | backgroundColor={getBackgroundColor()} 846 | linkColor={get3DLinkColor} 847 | linkThreeObjectExtend={true} 848 | linkThreeObject={(link) => { 849 | if (!showLinkLabels) new THREE.Object3D(); 850 | const sprite = new SpriteText(`${link.type}`); 851 | sprite.color = "lightgrey"; 852 | sprite.textHeight = 1.5; 853 | return sprite; 854 | }} 855 | linkPositionUpdate={(sprite, { start, end }) => { 856 | if (!showLinkLabels) return; 857 | 858 | const middlePos = ["x", "y", "z"].reduce((acc, c) => { 859 | acc[c as keyof Coords] = 860 | start[c as keyof Coords] + 861 | (end[c as keyof Coords] - start[c as keyof Coords]) / 2; 862 | return acc; 863 | }, {} as Coords); 864 | 865 | // Position sprite 866 | Object.assign(sprite.position, middlePos); 867 | }} 868 | /> 869 | )} 870 | 882 | Nodes: {nodeCount} 883 | Relationships: {linkCount} 884 | 891 | 900 | 901 | 902 | ); 903 | }; 904 | 905 | export default GraphViewer; 906 | -------------------------------------------------------------------------------- /src/app/components/Introduction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Typography, 4 | Box, 5 | Link, 6 | Paper, 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableContainer, 11 | TableHead, 12 | TableRow, 13 | Chip, 14 | } from "@mui/material"; 15 | 16 | const Introduction: React.FC = () => { 17 | return ( 18 | 19 | 20 | Welcome to the GraphRAG Visualizer 21 | 22 | 23 | 24 | If you are using GraphRAG 0.3.x or below, please visit 25 | the legacy site:{" "} 26 | 31 | GraphRAG Visualizer Legacy 32 | 33 | 34 | 35 | 36 | Overview 37 | 38 | 39 | This application visualizes Microsoft{" "} 40 | 45 | GraphRAG 46 | {" "} 47 | artifacts. Simply upload the parquet files to visualize the data without 48 | needing additional software like Gephi, Neo4j, or Jupyter Notebook. 49 | 50 | 51 | 57 | 58 | 59 | Features 60 | 61 |
    62 |
  • 63 | 64 | Graph Visualization: View the graph in 2D or 3D in 65 | the "Graph Visualization" tab. 66 | 67 |
  • 68 |
  • 69 | 70 | Data Tables: Display data from the parquet files in 71 | the "Data Tables" tab. 72 | 73 |
  • 74 |
  • 75 | 76 | Search Functionality: Fully supports search, 77 | allowing users to focus on specific nodes or relationships. 78 | 79 |
  • 80 |
  • 81 | 82 | Local Processing: Your artifacts are processed 83 | locally on your machine. They are not uploaded anywhere, ensuring 84 | your data remains secure and private. 85 | 86 |
  • 87 |
88 | 89 | 90 | Using the Search Functionality 91 | 92 | 93 | Once the{" "} 94 | 99 | graphrag-api 100 | {" "} 101 | server is up and running, you can perform searches directly through the 102 | GraphRAG Visualizer. This allows you to easily search and explore data 103 | that is hosted on your local server. 104 | 105 | 106 | 112 | 113 | 114 | Graph Data Model 115 | 116 | 117 | The logic for creating relationships for text units, documents, 118 | communities, and covariates is derived from the{" "} 119 | 124 | GraphRAG import Neo4j Cypher notebook 125 | 126 | . 127 | 128 | 129 | 130 | Nodes 131 | 132 | 133 | 134 | 135 | 136 | 137 | Node 138 | 139 | 140 | Type 141 | 142 | 143 | 144 | 145 | 146 | Document 147 | 148 | 149 | 150 | 151 | 152 | Text Unit 153 | 154 | 155 | 156 | 157 | 158 | Community 159 | 160 | 161 | 162 | 163 | 164 | Finding 165 | 166 | 167 | 168 | 169 | 170 | Covariate 171 | 172 | 173 | 174 | 175 | 176 | Entity 177 | 178 | Varies 179 | 180 | 181 | 182 |
183 |
184 | 185 | 186 | Relationships 187 | 188 | 189 | 190 | 191 | 192 | 193 | Source Node 194 | 195 | 196 | Relationship 197 | 198 | 199 | Target Node 200 | 201 | 202 | 203 | 204 | 205 | Entity 206 | 207 | 208 | 209 | Entity 210 | 211 | 212 | Text Unit 213 | 214 | 215 | 216 | Document 217 | 218 | 219 | Text Unit 220 | 221 | 222 | 223 | Entity 224 | 225 | 226 | Text Unit 227 | 228 | 229 | 230 | Covariate 231 | 232 | 233 | Community 234 | 235 | 236 | 237 | Finding 238 | 239 | 240 | Entity 241 | 242 | 243 | 244 | Community 245 | 246 | 247 |
248 |
249 |
250 | ); 251 | }; 252 | 253 | export default Introduction; 254 | -------------------------------------------------------------------------------- /src/app/components/SearchDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Button, 5 | Drawer, 6 | IconButton, 7 | InputAdornment, 8 | Paper, 9 | Table, 10 | TableBody, 11 | TableCell, 12 | TableContainer, 13 | TableHead, 14 | TableRow, 15 | TextField, 16 | Typography, 17 | } from "@mui/material"; 18 | import SearchIcon from "@mui/icons-material/Search"; 19 | import { CustomLink, CustomNode } from "../models/custom-graph-data"; 20 | 21 | interface SearchDrawerProps { 22 | searchTerm: string; 23 | setSearchTerm: React.Dispatch>; 24 | handleSearch: () => void; 25 | searchResults: (CustomNode | CustomLink)[]; 26 | rightDrawerOpen: boolean; 27 | toggleDrawer: (open: boolean) => () => void; 28 | handleFocusButtonClick: (node: CustomNode) => void; 29 | handleNodeClick: (node: CustomNode) => void; 30 | handleFocusLinkClick: (link: CustomLink) => void; 31 | handleLinkClick: (link: CustomLink) => void; 32 | } 33 | 34 | const SearchDrawer: React.FC = ({ 35 | searchTerm, 36 | setSearchTerm, 37 | handleSearch, 38 | searchResults, 39 | rightDrawerOpen, 40 | toggleDrawer, 41 | handleFocusButtonClick, 42 | handleNodeClick, 43 | handleFocusLinkClick, 44 | handleLinkClick, 45 | }) => { 46 | return ( 47 | 53 | 54 | setSearchTerm(e.target.value)} 57 | onKeyDown={(e) => { 58 | if (e.key === "Enter") { 59 | handleSearch(); 60 | } 61 | }} 62 | placeholder="Search Node or Relationship" 63 | fullWidth 64 | margin="normal" 65 | InputProps={{ 66 | endAdornment: ( 67 | 68 | 69 | 70 | 71 | 72 | ), 73 | }} 74 | /> 75 | 76 | {searchResults.filter((item) => "neighbors" in item).length > 0 && ( 77 | 78 | Nodes 79 | 83 | 84 | 85 | 86 | Name 87 | Type 88 | Actions 89 | 90 | 91 | 92 | {searchResults 93 | .filter((item) => "neighbors" in item) 94 | .map((node) => ( 95 | 96 | {node.name} 97 | {node.type} 98 | 99 | 100 | 107 | 115 | 116 | 117 | 118 | ))} 119 | 120 |
121 |
122 |
123 | )} 124 | 125 | {searchResults.filter((item) => "source" in item && "target" in item) 126 | .length > 0 && ( 127 | 128 | Relationships 129 | 133 | 134 | 135 | 136 | Source 137 | Target 138 | Description 139 | Action 140 | 141 | 142 | 143 | {searchResults 144 | .filter((item) => "source" in item && "target" in item) 145 | .map((link) => ( 146 | 147 | 148 | {typeof link.source === "object" 149 | ? (link.source as CustomNode).name 150 | : link.source} 151 | 152 | 153 | {typeof link.target === "object" 154 | ? (link.target as CustomNode).name 155 | : link.target} 156 | 157 | {link.description} 158 | 159 | 160 | 167 | 175 | 176 | 177 | 178 | ))} 179 | 180 |
181 |
182 |
183 | )} 184 |
185 |
186 | ); 187 | }; 188 | 189 | export default SearchDrawer; 190 | -------------------------------------------------------------------------------- /src/app/hooks/useFileHandler.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Entity } from "../models/entity"; 4 | import { Relationship } from "../models/relationship"; 5 | import { Document } from "../models/document"; 6 | import { TextUnit } from "../models/text-unit"; 7 | import { Community } from "../models/community"; 8 | import { CommunityReport } from "../models/community-report"; 9 | import { Covariate } from "../models/covariate"; 10 | import { readParquetFile } from "../utils/parquet-utils"; 11 | 12 | const baseFileNames = [ 13 | "entities.parquet", 14 | "relationships.parquet", 15 | "documents.parquet", 16 | "text_units.parquet", 17 | "communities.parquet", 18 | "community_reports.parquet", 19 | "covariates.parquet", 20 | ]; 21 | 22 | const baseMapping: { [key: string]: string } = { 23 | "entities.parquet": "entity", 24 | "relationships.parquet": "relationship", 25 | "documents.parquet": "document", 26 | "text_units.parquet": "text_unit", 27 | "communities.parquet": "community", 28 | "community_reports.parquet": "community_report", 29 | "covariates.parquet": "covariate", 30 | }; 31 | 32 | const fileSchemas: { [key: string]: string } = {}; 33 | Object.entries(baseMapping).forEach(([key, value]) => { 34 | fileSchemas[key] = value; 35 | fileSchemas[`create_final_${key}`] = value; 36 | }); 37 | 38 | const useFileHandler = () => { 39 | const navigate = useNavigate(); 40 | const [entities, setEntities] = useState([]); 41 | const [relationships, setRelationships] = useState([]); 42 | const [documents, setDocuments] = useState([]); 43 | const [textunits, setTextUnits] = useState([]); 44 | const [communities, setCommunities] = useState([]); 45 | const [covariates, setCovariates] = useState([]); 46 | const [communityReports, setCommunityReports] = useState( 47 | [] 48 | ); 49 | 50 | const handleFilesRead = async (files: File[]) => { 51 | await loadFiles(files); 52 | }; 53 | 54 | const loadFiles = async (files: File[] | string[]) => { 55 | const entitiesArray: Entity[][] = []; 56 | const relationshipsArray: Relationship[][] = []; 57 | const documentsArray: Document[][] = []; 58 | const textUnitsArray: TextUnit[][] = []; 59 | const communitiesArray: Community[][] = []; 60 | const communityReportsArray: CommunityReport[][] = []; 61 | const covariatesArray: Covariate[][] = []; 62 | 63 | for (const file of files) { 64 | const fileName = 65 | typeof file === "string" ? file.split("/").pop()! : file.name; 66 | const schema = fileSchemas[fileName] || fileSchemas[`create_final_${fileName}`]; 67 | 68 | let data; 69 | if (typeof file === "string") { 70 | // Fetch default file from public folder as binary data 71 | const response = await fetch(file); 72 | if (!response.ok) { 73 | console.error(`Failed to fetch file ${file}: ${response.statusText}`); 74 | continue; 75 | } 76 | 77 | // Convert ArrayBuffer to File object 78 | const buffer = await response.arrayBuffer(); 79 | const blob = new Blob([buffer], { type: "application/x-parquet" }); 80 | const fileBlob = new File([blob], fileName); 81 | 82 | // Use the File object in readParquetFile 83 | data = await readParquetFile(fileBlob, schema); 84 | // console.log(`Successfully loaded ${fileName} from public folder`); 85 | } else { 86 | // Handle drag-and-drop files directly 87 | data = await readParquetFile(file, schema); 88 | // console.log(`Successfully loaded ${file.name} from drag-and-drop`); 89 | } 90 | 91 | if (schema === "entity") { 92 | entitiesArray.push(data); 93 | } else if (schema === "relationship") { 94 | relationshipsArray.push(data); 95 | } else if (schema === "document") { 96 | documentsArray.push(data); 97 | } else if (schema === "text_unit") { 98 | textUnitsArray.push(data); 99 | } else if (schema === "community") { 100 | communitiesArray.push(data); 101 | } else if (schema === "community_report") { 102 | communityReportsArray.push(data); 103 | } else if (schema === "covariate") { 104 | covariatesArray.push(data); 105 | } 106 | } 107 | 108 | setEntities(entitiesArray.flat()); 109 | setRelationships(relationshipsArray.flat()); 110 | setDocuments(documentsArray.flat()); 111 | setTextUnits(textUnitsArray.flat()); 112 | setCommunities(communitiesArray.flat()); 113 | setCommunityReports(communityReportsArray.flat()); 114 | setCovariates(covariatesArray.flat()); 115 | }; 116 | 117 | const checkFileExists = async (filePath: string) => { 118 | try { 119 | const response = await fetch(filePath, { 120 | method: "HEAD", 121 | cache: "no-store", 122 | }); 123 | 124 | if (response.ok) { 125 | const contentType = response.headers.get("Content-Type"); 126 | 127 | if (contentType === "application/octet-stream") { 128 | // Updated Content-Type check 129 | console.log(`File exists: ${filePath}`); 130 | return true; 131 | } else { 132 | // console.warn( 133 | // `File does not exist or incorrect type: ${filePath} (Content-Type: ${contentType})` 134 | // ); 135 | return false; 136 | } 137 | } else { 138 | console.warn( 139 | `File does not exist: ${filePath} (status: ${response.status})` 140 | ); 141 | return false; 142 | } 143 | } catch (error) { 144 | console.error(`Error checking file existence for ${filePath}`, error); 145 | return false; 146 | } 147 | }; 148 | 149 | const loadDefaultFiles = async () => { 150 | const filesToLoad = []; 151 | 152 | for (const baseName of baseFileNames) { 153 | const prefixedPath = process.env.PUBLIC_URL + `/artifacts/create_final_${baseName}`; 154 | const unprefixedPath = process.env.PUBLIC_URL + `/artifacts/${baseName}`; 155 | 156 | if (await checkFileExists(prefixedPath)) { 157 | filesToLoad.push(prefixedPath); 158 | } else if (await checkFileExists(unprefixedPath)) { 159 | filesToLoad.push(unprefixedPath); 160 | } 161 | } 162 | 163 | if (filesToLoad.length > 0) { 164 | await loadFiles(filesToLoad); 165 | navigate("/graph", { replace: true }); 166 | } else { 167 | console.log("No default files found in the public folder."); 168 | } 169 | }; 170 | 171 | return { 172 | entities, 173 | relationships, 174 | documents, 175 | textunits, 176 | communities, 177 | covariates, 178 | communityReports, 179 | handleFilesRead, 180 | loadDefaultFiles, 181 | }; 182 | }; 183 | 184 | export default useFileHandler; 185 | -------------------------------------------------------------------------------- /src/app/hooks/useGraphData.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Entity } from "../models/entity"; 3 | import { Relationship } from "../models/relationship"; 4 | import { Document } from "../models/document"; 5 | import { TextUnit } from "../models/text-unit"; 6 | import { Community } from "../models/community"; 7 | import { CommunityReport } from "../models/community-report"; 8 | import { Covariate } from "../models/covariate"; 9 | import { CustomGraphData, CustomLink, CustomNode } from "../models/custom-graph-data"; 10 | 11 | const useGraphData = ( 12 | entities: Entity[], 13 | relationships: Relationship[], 14 | documents: Document[], 15 | textunits: TextUnit[], 16 | communities: Community[], 17 | communityReports: CommunityReport[], 18 | covariates: Covariate[], 19 | includeDocuments: boolean, 20 | includeTextUnits: boolean, 21 | includeCommunities: boolean, 22 | includeCovariates: boolean 23 | ) => { 24 | 25 | const [graphData, setGraphData] = useState({ nodes: [], links: [] }); 26 | useEffect(() => { 27 | const nodes: CustomNode[] = entities.map((entity) => ({ 28 | uuid: entity.id, 29 | id: entity.title, // use title as id because relationships use title as source/target 30 | name: entity.title, // legacy field for old GraphRAG 0.2.x - 0.3.x 31 | title: entity.title, // new field for GraphRAG 0.5.0+ (change name to title) 32 | type: entity.type, 33 | description: entity.description, 34 | human_readable_id: entity.human_readable_id, 35 | text_unit_ids: entity.text_unit_ids, 36 | neighbors: [], 37 | links: [], 38 | })); 39 | 40 | const nodesMap: { [key: string]: CustomNode } = {}; 41 | nodes.forEach(node => nodesMap[node.id] = node); 42 | 43 | const links: CustomLink[] = relationships 44 | .map((relationship) => ({ 45 | source: relationship.source, 46 | target: relationship.target, 47 | type: relationship.type, 48 | weight: relationship.weight, 49 | description: relationship.description, 50 | text_unit_ids: relationship.text_unit_ids, 51 | id: relationship.id, 52 | human_readable_id: relationship.human_readable_id, 53 | combined_degree: relationship.combined_degree, 54 | })) 55 | .filter((link) => nodesMap[link.source] && nodesMap[link.target]); 56 | 57 | 58 | 59 | if (includeDocuments) { 60 | const documentNodes = documents.map((document) => ({ 61 | uuid: document.id, 62 | id: document.id, 63 | name: document.title, 64 | title: document.title, 65 | type: "RAW_DOCUMENT", // avoid conflict with "DOCUMENT" type 66 | text: document.text, 67 | text_unit_ids: document.text_unit_ids, 68 | human_readable_id: document.human_readable_id, 69 | neighbors: [], 70 | links: [], 71 | })); 72 | 73 | documentNodes.forEach(node => nodesMap[node.id] = node); 74 | nodes.push(...documentNodes); 75 | 76 | if (includeTextUnits) { 77 | const textUnitDocumentLinks = textunits 78 | .filter((textunit) => (textunit.document_ids ?? []).length > 0) 79 | .flatMap((textunit) => 80 | textunit.document_ids.map((documentId) => ({ 81 | source: textunit.id, 82 | target: documentId, 83 | type: "PART_OF", 84 | id: `${textunit.id}-${documentId}`, 85 | })) 86 | ); 87 | 88 | links.push(...textUnitDocumentLinks); 89 | } 90 | } 91 | 92 | if (includeTextUnits) { 93 | const textUnitNodes = textunits.map((textunit) => ({ 94 | uuid: textunit.id, 95 | id: textunit.id, 96 | name: `TEXT UNIT ${textunit.id}`, 97 | type: "CHUNK", 98 | text: textunit.text, 99 | n_tokens: textunit.n_tokens, 100 | document_ids: textunit.document_ids, 101 | entity_ids: textunit.entity_ids, 102 | relationship_ids: textunit.relationship_ids, 103 | human_readable_id: textunit.human_readable_id, 104 | neighbors: [], 105 | links: [], 106 | })); 107 | 108 | textUnitNodes.forEach(node => nodesMap[node.id] = node); 109 | nodes.push(...textUnitNodes); 110 | 111 | const textUnitEntityLinks = textunits 112 | .filter((textunit) => (textunit.entity_ids ?? []).length > 0) 113 | .flatMap((textunit) => 114 | textunit.entity_ids.map((entityId) => ({ 115 | source: textunit.id, 116 | target: nodes.find((e) => e.uuid === entityId)?.name || "", 117 | type: "HAS_ENTITY", 118 | id: `${textunit.id}-${entityId}`, 119 | })) 120 | ); 121 | 122 | links.push(...textUnitEntityLinks); 123 | } 124 | 125 | if (includeCommunities) { 126 | const communityNodes = communities.map((community) => { 127 | const report = communityReports.find( 128 | (r) => r.community.toString() === community.community.toString() 129 | ); 130 | return { 131 | uuid: community.id.toString(), 132 | id: community.id.toString(), 133 | name: community.title, 134 | type: "COMMUNITY", 135 | entity_ids: community.text_unit_ids, 136 | relationship_ids: community.relationship_ids, 137 | full_content: report?.full_content || "", 138 | level: report?.level || -1, 139 | rank: report?.rank || -1, 140 | title: report?.title || "", 141 | rank_explanation: report?.rank_explanation || "", 142 | summary: report?.summary || "", 143 | findings: report?.findings || [], 144 | neighbors: [], 145 | links: [], 146 | }; 147 | }); 148 | communityNodes.forEach(node => nodesMap[node.id] = node); 149 | nodes.push(...communityNodes); 150 | 151 | const uniqueLinks = new Set(); 152 | const communityEntityLinks = communities 153 | .flatMap((community) => 154 | community.relationship_ids.map((relId) => { 155 | const relationship = relationships.find((rel) => rel.id === relId); 156 | if (!relationship) return []; 157 | 158 | const sourceLinkId = `${relationship.source}-${community.id}`; 159 | const targetLinkId = `${relationship.target}-${community.id}`; 160 | 161 | const newLinks = []; 162 | 163 | if (!uniqueLinks.has(sourceLinkId)) { 164 | uniqueLinks.add(sourceLinkId); 165 | newLinks.push({ 166 | source: relationship.source, 167 | target: community.id.toString(), 168 | type: "IN_COMMUNITY", 169 | id: sourceLinkId, 170 | }); 171 | } 172 | 173 | if (!uniqueLinks.has(targetLinkId)) { 174 | uniqueLinks.add(targetLinkId); 175 | newLinks.push({ 176 | source: relationship.target, 177 | target: community.id.toString(), 178 | type: "IN_COMMUNITY", 179 | id: targetLinkId, 180 | }); 181 | } 182 | 183 | return newLinks; 184 | }) 185 | ) 186 | .flat(); 187 | 188 | links.push(...communityEntityLinks); 189 | 190 | //Add finding nodes and links 191 | communityNodes.forEach((communityNode) => { 192 | if (communityNode.findings) { 193 | communityNode.findings.forEach((finding, idx) => { 194 | const findingNode = { 195 | uuid: `community-${communityNode.uuid}-finding-${idx}`, 196 | id: `${communityNode.id}-finding-${idx}`, 197 | name: `${communityNode.title}-finding-${idx}`, 198 | type: "FINDING", 199 | explanation: finding.explanation, 200 | summary: finding.summary, 201 | neighbors: [], 202 | links: [], 203 | }; 204 | 205 | nodesMap[findingNode.id] = findingNode; 206 | nodes.push(findingNode); 207 | 208 | const findingLink = { 209 | source: communityNode.id, 210 | target: findingNode.id, 211 | type: "HAS_FINDING", 212 | id: `${communityNode.id}-finding-${idx}`, 213 | }; 214 | 215 | links.push(findingLink); 216 | }); 217 | } 218 | }); 219 | } 220 | 221 | if (includeCovariates) { 222 | const covariateNodes = covariates.map((covariate) => ({ 223 | uuid: covariate.id, 224 | id: covariate.id, 225 | human_readable_id: covariate.human_readable_id, 226 | name: `COVARIATE ${covariate.id}`, 227 | covariate_type: covariate.covariate_type, 228 | // type: "COVARIATE", 229 | type: covariate.type, 230 | description: covariate.description || "", 231 | subject_id: covariate.subject_id, 232 | object_id: covariate.object_id, 233 | status: covariate.status, 234 | start_date: covariate.start_date, 235 | end_date: covariate.end_date, 236 | source_text: covariate.source_text, 237 | text_unit_id: covariate.text_unit_id, 238 | neighbors: [], 239 | links: [], 240 | })); 241 | 242 | covariateNodes.forEach(node => nodesMap[node.id] = node); 243 | nodes.push(...covariateNodes); 244 | 245 | const covariateTextUnitLinks = covariates.map((covariate) => ({ 246 | source: covariate.text_unit_id, 247 | target: covariate.id, 248 | type: "HAS_COVARIATE", 249 | id: `${covariate.text_unit_id}-${covariate.id}`, 250 | })); 251 | 252 | links.push(...covariateTextUnitLinks); 253 | } 254 | 255 | links.forEach(link => { 256 | const sourceNode = nodesMap[link.source]; 257 | const targetNode = nodesMap[link.target]; 258 | if (sourceNode && targetNode) { 259 | if (!sourceNode.neighbors!.includes(targetNode)) 260 | sourceNode.neighbors!.push(targetNode); 261 | if (!targetNode.neighbors!.includes(sourceNode)) 262 | targetNode.neighbors!.push(sourceNode); 263 | if (!sourceNode.links!.includes(link)) 264 | sourceNode.links!.push(link); 265 | if (!targetNode.links!.includes(link)) 266 | targetNode.links!.push(link); 267 | } 268 | }); 269 | 270 | 271 | if (nodes.length > 0) { 272 | setGraphData({ nodes, links }); 273 | } 274 | }, [ 275 | entities, 276 | relationships, 277 | documents, 278 | textunits, 279 | communities, 280 | communityReports, 281 | covariates, 282 | includeDocuments, 283 | includeTextUnits, 284 | includeCommunities, 285 | includeCovariates, 286 | ]); 287 | 288 | return graphData; 289 | }; 290 | 291 | export default useGraphData; 292 | -------------------------------------------------------------------------------- /src/app/layout/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | HashRouter as Router, 4 | Routes, 5 | Route, 6 | Navigate, 7 | } from "react-router-dom"; 8 | import ReactGA from "react-ga4"; 9 | 10 | import GraphDataHandler from "../components/GraphDataHandler"; 11 | import { 12 | CssBaseline, 13 | Container, 14 | Box, 15 | createTheme, 16 | darkScrollbar, 17 | ThemeProvider, 18 | IconButton, 19 | Tooltip, 20 | Link, 21 | } from "@mui/material"; 22 | import LightModeOutlinedIcon from "@mui/icons-material/LightModeOutlined"; 23 | import DarkModeOutlinedIcon from "@mui/icons-material/DarkModeOutlined"; 24 | import GitHubIcon from "@mui/icons-material/GitHub"; 25 | 26 | const App: React.FC = () => { 27 | const [darkMode, setDarkMode] = useState(true); 28 | const paletteType = darkMode ? "dark" : "light"; 29 | 30 | const theme = createTheme({ 31 | palette: { 32 | mode: paletteType, 33 | }, 34 | components: { 35 | MuiCssBaseline: { 36 | styleOverrides: { 37 | body: paletteType === "dark" ? darkScrollbar() : null, 38 | }, 39 | }, 40 | MuiPopover: { 41 | styleOverrides: { 42 | root: { 43 | zIndex: 1600, 44 | }, 45 | }, 46 | }, 47 | MuiModal: { 48 | styleOverrides: { 49 | root: { 50 | zIndex: 1600, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }); 56 | 57 | function handleThemeChange() { 58 | setDarkMode(!darkMode); 59 | localStorage.setItem("theme", darkMode ? "light" : "dark"); 60 | } 61 | 62 | useEffect(() => { 63 | const currentTheme = localStorage.getItem("theme"); 64 | setDarkMode(currentTheme === "dark"); 65 | }, []); 66 | 67 | useEffect(() => { 68 | const measurementId = process.env.REACT_APP_GA_MEASUREMENT_ID; 69 | if (measurementId) { 70 | ReactGA.initialize(measurementId); 71 | ReactGA.send({ 72 | hitType: "pageview", 73 | page: window.location.pathname + window.location.search, 74 | }); 75 | } else { 76 | console.error("Google Analytics measurement ID not found"); 77 | } 78 | }, []); 79 | 80 | return ( 81 | 82 | 83 | 84 | 85 | 86 | 99 | 106 | 107 | 108 | {darkMode ? ( 109 | 110 | 111 | 112 | 113 | 114 | ) : ( 115 | 116 | 117 | 118 | 119 | 120 | )} 121 | 122 | 123 | } />{" "} 124 | } />{" "} 125 | } />{" "} 126 | } />{" "} 127 | } />{" "} 128 | 129 | 130 | {/* */} 131 | 132 | 133 | 134 | ); 135 | }; 136 | 137 | export default App; 138 | -------------------------------------------------------------------------------- /src/app/models/community-report.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | 3 | export interface Finding { 4 | explanation: string; 5 | summary: string; 6 | } 7 | 8 | export interface CommunityReport { 9 | id: string; 10 | human_readable_id: number; 11 | community: number; 12 | parent?: number; 13 | level: number; 14 | title: string; 15 | summary: string; 16 | full_content: string; 17 | rank: number; 18 | rank_explanation: string; 19 | findings: Finding[]; 20 | full_content_json: string; 21 | period: string; 22 | size: number; 23 | } 24 | 25 | export const findingColumns: MRT_ColumnDef[] = [ 26 | { 27 | accessorKey: "id", 28 | header: "id", 29 | }, 30 | { 31 | accessorKey: "explanation", 32 | header: "explanation", 33 | }, 34 | { 35 | accessorKey: "summary", 36 | header: "summary", 37 | }, 38 | 39 | ] 40 | 41 | export const communityReportColumns: MRT_ColumnDef[] = [ 42 | { 43 | accessorKey: "id", 44 | header: "id", 45 | }, 46 | { 47 | accessorKey: "human_readable_id", 48 | header: "human_readable_id", 49 | }, 50 | { 51 | accessorKey: "community", 52 | header: "community", 53 | }, 54 | { 55 | accessorKey: "parent", 56 | header: "parent", 57 | }, 58 | { 59 | accessorKey: "level", 60 | header: "level", 61 | }, 62 | { 63 | accessorKey: "title", 64 | header: "title", 65 | }, 66 | { 67 | accessorKey: "summary", 68 | header: "summary", 69 | }, 70 | { 71 | accessorKey: "full_content", 72 | header: "full_content", 73 | }, 74 | { 75 | accessorKey: "rank", 76 | header: "rank", 77 | }, 78 | { 79 | accessorKey: "rank_explanation", 80 | header: "rank_explanation", 81 | }, 82 | { 83 | accessorKey: "findings", 84 | header: "findings", 85 | Cell: ({ renderedCellValue }) => 86 | Array.isArray(renderedCellValue) 87 | ? JSON.stringify(renderedCellValue, null, 2) 88 | : renderedCellValue, 89 | }, 90 | { 91 | accessorKey: "full_content_json", 92 | header: "full_content_json", 93 | }, 94 | { 95 | accessorKey: "period", 96 | header: "period", 97 | }, 98 | { 99 | accessorKey: "size", 100 | header: "size", 101 | }, 102 | ]; -------------------------------------------------------------------------------- /src/app/models/community.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | 3 | export interface Community { 4 | id: number; 5 | human_readable_id: number; 6 | community: number; 7 | parent?: number; 8 | level: number; 9 | title: string; 10 | entity_ids: string[]; 11 | relationship_ids: string[]; 12 | text_unit_ids: string[]; 13 | period: string; 14 | size: number; 15 | } 16 | 17 | export const communityColumns: MRT_ColumnDef[] = [ 18 | { 19 | accessorKey: "id", 20 | header: "id", 21 | }, 22 | { 23 | accessorKey: "human_readable_id", 24 | header: "human_readable_id", 25 | }, 26 | { 27 | accessorKey: "community", 28 | header: "community", 29 | }, 30 | { 31 | accessorKey: "parent", 32 | header: "parent", 33 | }, 34 | { 35 | accessorKey: "level", 36 | header: "level", 37 | }, 38 | { 39 | accessorKey: "title", 40 | header: "title", 41 | }, 42 | { 43 | accessorKey: "entity_ids", 44 | header: "entity_ids", 45 | Cell: ({ renderedCellValue }) => 46 | Array.isArray(renderedCellValue) 47 | ? JSON.stringify(renderedCellValue, null, 2) 48 | : renderedCellValue, 49 | }, 50 | { 51 | accessorKey: "relationship_ids", 52 | header: "relationship_ids", 53 | Cell: ({ renderedCellValue }) => 54 | Array.isArray(renderedCellValue) 55 | ? JSON.stringify(renderedCellValue, null, 2) 56 | : renderedCellValue, 57 | }, 58 | { 59 | accessorKey: "text_unit_ids", 60 | header: "text_unit_ids", 61 | Cell: ({ renderedCellValue }) => 62 | Array.isArray(renderedCellValue) 63 | ? JSON.stringify(renderedCellValue, null, 2) 64 | : renderedCellValue, 65 | }, 66 | { 67 | accessorKey: "period", 68 | header: "period", 69 | }, 70 | { 71 | accessorKey: "size", 72 | header: "size", 73 | }, 74 | ]; -------------------------------------------------------------------------------- /src/app/models/covariate.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | 3 | export interface Covariate { 4 | id: string; 5 | human_readable_id: number; 6 | covariate_type: string; 7 | type: string; 8 | description: string; 9 | subject_id: string; 10 | object_id: string; 11 | status: string; 12 | start_date: string; 13 | end_date: string; 14 | source_text: string; 15 | text_unit_id: string; 16 | } 17 | 18 | export const covariateColumns: MRT_ColumnDef[] = [ 19 | { 20 | accessorKey: "id", 21 | header: "id", 22 | }, 23 | { 24 | accessorKey: "human_readable_id", 25 | header: "human_readable_id", 26 | }, 27 | { 28 | accessorKey: "covariate_type", 29 | header: "covariate_type", 30 | }, 31 | { 32 | accessorKey: "type", 33 | header: "type", 34 | }, 35 | { 36 | accessorKey: "description", 37 | header: "description", 38 | }, 39 | { 40 | accessorKey: "subject_id", 41 | header: "subject_id", 42 | }, 43 | { 44 | accessorKey: "object_id", 45 | header: "object_id", 46 | }, 47 | { 48 | accessorKey: "status", 49 | header: "status", 50 | }, 51 | { 52 | accessorKey: "start_date", 53 | header: "start_date", 54 | }, 55 | { 56 | accessorKey: "end_date", 57 | header: "end_date", 58 | }, 59 | { 60 | accessorKey: "source_text", 61 | header: "source_text", 62 | }, 63 | { 64 | accessorKey: "text_unit_id", 65 | header: "text_unit_id", 66 | }, 67 | ]; -------------------------------------------------------------------------------- /src/app/models/custom-graph-data.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | import { 3 | GraphData, 4 | NodeObject, 5 | LinkObject, 6 | } from "react-force-graph-2d"; 7 | import { Finding } from "./community-report"; 8 | 9 | export interface CustomNode extends NodeObject { 10 | uuid: string; 11 | id: string; 12 | name: string; 13 | type: string; 14 | title?: string; 15 | description?: string; 16 | human_readable_id?: number; 17 | graph_embedding?: number[]; 18 | text_unit_ids?: string[]; 19 | description_embedding?: number[]; 20 | neighbors?: CustomNode[]; 21 | links?: CustomLink[]; 22 | text?: string; 23 | n_tokens?: number; 24 | document_ids?: string[]; 25 | entity_ids?: string[]; 26 | relationship_ids?: string[]; 27 | level?: number; 28 | raw_community?: number; 29 | raw_content?: string; 30 | rank?: number; 31 | rank_explanation?: string; 32 | summary?: string; 33 | findings?: Finding[] 34 | full_content?: string; 35 | explanation?: string; 36 | subject_id?: string; 37 | object_id?: string; 38 | status?: string; 39 | start_date?: string; 40 | end_date?: string; 41 | source_text?: string; 42 | text_unit_id?: string; 43 | covariate_type?: string; 44 | parent?: number; 45 | } 46 | 47 | export interface CustomLink extends LinkObject { 48 | source: string; 49 | target: string; 50 | type: string; 51 | weight?: number; 52 | description?: string; 53 | text_unit_ids?: string[]; 54 | id: string; 55 | human_readable_id?: number; 56 | combined_degree?: number; 57 | source_degree?: number; 58 | target_degree?: number; 59 | rank?: number; 60 | } 61 | 62 | export interface CustomGraphData extends GraphData { 63 | nodes: CustomNode[]; 64 | links: CustomLink[]; 65 | } 66 | 67 | 68 | export const customNodeColumns: MRT_ColumnDef[] = [ 69 | { 70 | accessorKey: "uuid", 71 | header: "id", 72 | }, 73 | { 74 | accessorKey: "human_readable_id", 75 | header: "human_readable_id", 76 | }, 77 | { 78 | accessorKey: "name", 79 | header: "name", 80 | }, 81 | { 82 | accessorKey: "title", 83 | header: "title", 84 | }, 85 | { 86 | accessorKey: "type", 87 | header: "type", 88 | }, 89 | { 90 | accessorKey: "description", 91 | header: "description", 92 | }, 93 | { 94 | accessorKey: "graph_embedding", 95 | header: "graph_embedding", 96 | Cell: ({ renderedCellValue }) => 97 | Array.isArray(renderedCellValue) 98 | ? JSON.stringify(renderedCellValue, null, 2) 99 | : renderedCellValue, 100 | }, 101 | { 102 | accessorKey: "text_unit_ids", 103 | header: "text_unit_ids", 104 | Cell: ({ renderedCellValue }) => 105 | Array.isArray(renderedCellValue) 106 | ? JSON.stringify(renderedCellValue, null, 2) 107 | : renderedCellValue, 108 | }, 109 | { 110 | accessorKey: "description_embedding", 111 | header: "description_embedding", 112 | Cell: ({ renderedCellValue }) => 113 | Array.isArray(renderedCellValue) 114 | ? JSON.stringify(renderedCellValue, null, 2) 115 | : renderedCellValue, 116 | }, 117 | { 118 | accessorKey: "level", 119 | header: "level", 120 | }, 121 | { 122 | accessorKey: "n_tokens", 123 | header: "n_tokens", 124 | }, 125 | { 126 | accessorKey: "rank", 127 | header: "rank", 128 | }, 129 | { 130 | accessorKey: "rank_explanation", 131 | header: "rank_explanation", 132 | }, 133 | { 134 | accessorKey: "summary", 135 | header: "summary", 136 | }, 137 | { 138 | accessorKey: "full_content", 139 | header: "full_content", 140 | }, 141 | { 142 | accessorKey: "explanation", 143 | header: "explanation", 144 | }, 145 | { 146 | accessorKey: "findings", 147 | header: "findings", 148 | Cell: ({ renderedCellValue }) => 149 | Array.isArray(renderedCellValue) 150 | ? JSON.stringify(renderedCellValue, null, 2) 151 | : renderedCellValue, 152 | }, 153 | { 154 | accessorKey: "text", 155 | header: "text", 156 | }, 157 | { 158 | accessorKey: "document_ids", 159 | header: "document_ids", 160 | Cell: ({ renderedCellValue }) => 161 | Array.isArray(renderedCellValue) 162 | ? JSON.stringify(renderedCellValue, null, 2) 163 | : renderedCellValue, 164 | }, 165 | { 166 | accessorKey: "entity_ids", 167 | header: "entity_ids", 168 | Cell: ({ renderedCellValue }) => 169 | Array.isArray(renderedCellValue) 170 | ? JSON.stringify(renderedCellValue, null, 2) 171 | : renderedCellValue, 172 | }, 173 | { 174 | accessorKey: "relationship_ids", 175 | header: "relationship_ids", 176 | Cell: ({ renderedCellValue }) => 177 | Array.isArray(renderedCellValue) 178 | ? JSON.stringify(renderedCellValue, null, 2) 179 | : renderedCellValue, 180 | }, 181 | 182 | ]; 183 | 184 | export const customLinkColumns: MRT_ColumnDef[] = [ 185 | { 186 | accessorKey: "source", 187 | header: "source", 188 | }, 189 | { 190 | accessorKey: "target", 191 | header: "target", 192 | }, 193 | { 194 | accessorKey: "type", 195 | header: "type", 196 | }, 197 | { 198 | accessorKey: "weight", 199 | header: "weight", 200 | }, 201 | { 202 | accessorKey: "description", 203 | header: "description", 204 | }, 205 | { 206 | accessorKey: "text_unit_ids", 207 | header: "text_unit_ids", 208 | Cell: ({ renderedCellValue }) => 209 | Array.isArray(renderedCellValue) 210 | ? JSON.stringify(renderedCellValue, null, 2) 211 | : renderedCellValue, 212 | }, 213 | { 214 | accessorKey: "id", 215 | header: "id", 216 | }, 217 | { 218 | accessorKey: "human_readable_id", 219 | header: "human_readable_id", 220 | }, 221 | { 222 | accessorKey: "source_degree", 223 | header: "source_degree", 224 | }, 225 | { 226 | accessorKey: "target_degree", 227 | header: "target_degree", 228 | }, 229 | { 230 | accessorKey: "combined_degree", 231 | header: "combined_degree", 232 | }, 233 | { 234 | accessorKey: "rank", 235 | header: "rank", 236 | }, 237 | ]; -------------------------------------------------------------------------------- /src/app/models/document.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | 3 | export interface Document { 4 | id: string; 5 | human_readable_id: number; 6 | title: string; 7 | text: string; 8 | text_unit_ids: string[]; 9 | } 10 | 11 | export const documentColumns: MRT_ColumnDef[] = [ 12 | { 13 | accessorKey: "id", 14 | header: "id", 15 | }, 16 | { 17 | accessorKey: "human_readable_id", 18 | header: "human_readable_id", 19 | }, 20 | { 21 | accessorKey: "title", 22 | header: "title", 23 | }, 24 | { 25 | accessorKey: "text", 26 | header: "text", 27 | }, 28 | { 29 | accessorKey: "text_unit_ids", 30 | header: "text_unit_ids", 31 | Cell: ({ renderedCellValue }) => 32 | Array.isArray(renderedCellValue) 33 | ? JSON.stringify(renderedCellValue, null, 2) 34 | : renderedCellValue, 35 | }, 36 | ]; -------------------------------------------------------------------------------- /src/app/models/entity.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | 3 | export interface Entity { 4 | id: string; 5 | human_readable_id: number; 6 | title: string; 7 | type: string; 8 | description: string; 9 | text_unit_ids: string[]; 10 | } 11 | 12 | export const entityColumns: MRT_ColumnDef[] = [ 13 | { 14 | accessorKey: "id", 15 | header: "id", 16 | }, 17 | { 18 | accessorKey: "human_readable_id", 19 | header: "human_readable_id", 20 | }, 21 | { 22 | accessorKey: "title", 23 | header: "title", 24 | }, 25 | { 26 | accessorKey: "type", 27 | header: "type", 28 | }, 29 | { 30 | accessorKey: "description", 31 | header: "description", 32 | }, 33 | { 34 | accessorKey: "text_unit_ids", 35 | header: "text_unit_ids", 36 | Cell: ({ renderedCellValue }) => 37 | Array.isArray(renderedCellValue) 38 | ? JSON.stringify(renderedCellValue, null, 2) 39 | : renderedCellValue, 40 | }, 41 | ]; -------------------------------------------------------------------------------- /src/app/models/relationship.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | 3 | export interface Relationship { 4 | id: string; 5 | human_readable_id: number; 6 | source: string; 7 | target: string; 8 | description: string; 9 | weight: number; 10 | combined_degree: number; 11 | text_unit_ids: string[]; 12 | type: string; // Custom field to match neo4j 13 | } 14 | 15 | export const relationshipColumns: MRT_ColumnDef[] = [ 16 | { 17 | accessorKey: "id", 18 | header: "id", 19 | }, 20 | { 21 | accessorKey: "human_readable_id", 22 | header: "human_readable_id", 23 | }, 24 | { 25 | accessorKey: "source", 26 | header: "source", 27 | }, 28 | { 29 | accessorKey: "target", 30 | header: "target", 31 | }, 32 | { 33 | accessorKey: "description", 34 | header: "description", 35 | }, 36 | { 37 | accessorKey: "weight", 38 | header: "weight", 39 | }, 40 | { 41 | accessorKey: "text_unit_ids", 42 | header: "text_unit_ids", 43 | Cell: ({ renderedCellValue }) => 44 | Array.isArray(renderedCellValue) 45 | ? JSON.stringify(renderedCellValue, null, 2) 46 | : renderedCellValue, 47 | }, 48 | { 49 | accessorKey: "combined_degree", 50 | header: "combined_degree", 51 | }, 52 | { 53 | accessorKey: "type", 54 | header: "type", 55 | }, 56 | ]; -------------------------------------------------------------------------------- /src/app/models/search-result.ts: -------------------------------------------------------------------------------- 1 | export interface SearchResult { 2 | // response: string | Record | Array>; 3 | response: string; 4 | context_data: string | Array> | Record>>; 5 | context_text: string | string[] | Record; 6 | completion_time: number; 7 | llm_calls: number; 8 | prompt_tokens: number; 9 | reduce_context_data?: string | Array> | Record>>; 10 | reduce_context_text?: string | string[] | Record; 11 | map_responses?: Array; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/models/text-unit.ts: -------------------------------------------------------------------------------- 1 | import { MRT_ColumnDef } from "material-react-table"; 2 | 3 | export interface TextUnit { 4 | id: string; 5 | human_readable_id: number; 6 | text: string; 7 | n_tokens: number; 8 | document_ids: string[]; 9 | entity_ids: string[]; 10 | relationship_ids: string[]; 11 | } 12 | 13 | export const textUnitColumns: MRT_ColumnDef[] = [ 14 | { 15 | accessorKey: "id", 16 | header: "id", 17 | }, 18 | { 19 | accessorKey: "text", 20 | header: "text", 21 | }, 22 | { 23 | accessorKey: "n_tokens", 24 | header: "n_tokens", 25 | }, 26 | { 27 | accessorKey: "document_ids", 28 | header: "document_ids", 29 | Cell: ({ renderedCellValue }) => 30 | Array.isArray(renderedCellValue) 31 | ? JSON.stringify(renderedCellValue, null, 2) 32 | : renderedCellValue, 33 | }, 34 | { 35 | accessorKey: "entity_ids", 36 | header: "entity_ids", 37 | Cell: ({ renderedCellValue }) => 38 | Array.isArray(renderedCellValue) 39 | ? JSON.stringify(renderedCellValue, null, 2) 40 | : renderedCellValue, 41 | }, 42 | { 43 | accessorKey: "relationship_ids", 44 | header: "relationship_ids", 45 | Cell: ({ renderedCellValue }) => 46 | Array.isArray(renderedCellValue) 47 | ? JSON.stringify(renderedCellValue, null, 2) 48 | : renderedCellValue, 49 | }, 50 | ]; -------------------------------------------------------------------------------- /src/app/utils/parquet-utils.ts: -------------------------------------------------------------------------------- 1 | import { parquetRead, ParquetReadOptions } from "hyparquet"; 2 | 3 | export class AsyncBuffer { 4 | private buffer: ArrayBuffer; 5 | 6 | constructor(buffer: ArrayBuffer) { 7 | this.buffer = buffer; 8 | } 9 | 10 | async slice(start: number, end: number): Promise { 11 | return this.buffer.slice(start, end); 12 | } 13 | 14 | get byteLength() { 15 | return this.buffer.byteLength; 16 | } 17 | } 18 | 19 | const parseValue = (value: any, type: "number" | "bigint"): any => { 20 | if (typeof value === "string" && value.endsWith("n")) { 21 | return BigInt(value.slice(0, -1)); 22 | } 23 | return type === "bigint" ? BigInt(value) : Number(value); 24 | }; 25 | 26 | export const readParquetFile = async ( 27 | file: File | Blob, 28 | schema?: string 29 | ): Promise => { 30 | try { 31 | const arrayBuffer = await file.arrayBuffer(); 32 | const asyncBuffer = new AsyncBuffer(arrayBuffer); 33 | 34 | return new Promise((resolve, reject) => { 35 | const options: ParquetReadOptions = { 36 | file: asyncBuffer, 37 | rowFormat: 'object', 38 | onComplete: (rows: Record[]) => { 39 | if (schema === "entity") { 40 | 41 | resolve( 42 | rows.map((row) => ({ 43 | id: row["id"], 44 | human_readable_id: parseValue(row["human_readable_id"], "number"), 45 | title: row["title"], 46 | type: row["type"], 47 | description: row["description"], 48 | text_unit_ids: row["text_unit_ids"], 49 | 50 | })) 51 | ); 52 | } else if (schema === "relationship") { 53 | resolve( 54 | rows.map((row) => ({ 55 | id: row["id"], 56 | human_readable_id: parseValue(row["human_readable_id"], "number"), 57 | source: row["source"], 58 | target: row["target"], 59 | description: row["description"], 60 | weight: row["weight"], 61 | combined_degree: parseValue(row["combined_degree"], "number"), 62 | text_unit_ids: row["text_unit_ids"], 63 | type: "RELATED", // Custom field to match neo4j 64 | })) 65 | ); 66 | } else if (schema === "document") { 67 | resolve( 68 | rows.map((row) => ({ 69 | id: row["id"], 70 | human_readable_id: parseValue(row["human_readable_id"], "number"), 71 | title: row["title"], 72 | text: row["text"], 73 | text_unit_ids: row["text_unit_ids"], 74 | })) 75 | ); 76 | } else if (schema === "text_unit") { 77 | resolve( 78 | rows.map((row) => ({ 79 | id: row["id"], 80 | human_readable_id: parseValue(row["human_readable_id"], "number"), 81 | text: row["text"], 82 | n_tokens: parseValue(row["n_tokens"], "number"), 83 | document_ids: row["document_ids"], 84 | entity_ids: row["entity_ids"], 85 | relationship_ids: row["relationship_ids"], 86 | })) 87 | ); 88 | } else if (schema === "community") { 89 | resolve( 90 | rows.map((row) => ({ 91 | id: row["id"], 92 | human_readable_id: parseValue(row["human_readable_id"], "number"), 93 | community: parseValue(row["community"], "number"), 94 | parent: parseValue(row["parent"], "number"), 95 | level: parseValue(row["level"], "number"), 96 | title: row["title"], 97 | entity_ids: row["entity_ids"], 98 | relationship_ids: row["relationship_ids"], 99 | text_unit_ids: row["text_unit_ids"], 100 | period: row["period"], 101 | size: parseValue(row["size"], "number"), 102 | })) 103 | ); 104 | } else if (schema === "community_report") { 105 | resolve( 106 | rows.map((row) => ({ 107 | id: row["id"], 108 | human_readable_id: parseValue(row["human_readable_id"], "number"), 109 | community: parseValue(row["community"], "number"), 110 | parent: parseValue(row["parent"], "number"), 111 | level: parseValue(row["level"], "number"), 112 | title: row["title"], 113 | summary: row["summary"], 114 | full_content: row["full_content"], 115 | rank: row["rank"], 116 | rank_explanation: row["rank_explanation"], 117 | findings: row["findings"], 118 | full_content_json: row["full_content_json"], 119 | period: row["period"], 120 | size: parseValue(row["size"], "number"), 121 | })) 122 | ); 123 | } else if (schema === "covariate") { 124 | resolve( 125 | rows.map((row) => ({ 126 | id: row["id"], 127 | human_readable_id: parseValue(row["human_readable_id"], "number"), 128 | covariate_type: row["covariate_type"], 129 | type: row["type"], 130 | description: row["description"], 131 | subject_id: row["subject_id"], 132 | object_id: row["object_id"], 133 | status: row["status"], 134 | start_date: row["start_date"], 135 | end_date: row["end_date"], 136 | source_text: row["source_text"], 137 | text_unit_id: row["text_unit_id"], 138 | })) 139 | ); 140 | } else { 141 | resolve( 142 | rows.map((row: Record) => ({ 143 | ...row 144 | })) 145 | ); 146 | } 147 | }, 148 | }; 149 | parquetRead(options).catch(reject); 150 | }); 151 | } catch (err) { 152 | console.error("Error reading Parquet file", err); 153 | return []; 154 | } 155 | }; 156 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /src/hyparquet.d.ts: -------------------------------------------------------------------------------- 1 | import type { AsyncBuffer, Compressors, FileMetaData, SchemaTree } from './types.d.ts' 2 | 3 | export type { AsyncBuffer, Compressors, FileMetaData, SchemaTree } 4 | 5 | /** 6 | * Read parquet data rows from a file-like object. 7 | * Reads the minimal number of row groups and columns to satisfy the request. 8 | * 9 | * Returns a void promise when complete, and to throw errors. 10 | * Data is returned in onComplete, not the return promise, because 11 | * if onComplete is undefined, we parse the data, and emit chunks, but skip 12 | * computing the row view directly. This saves on allocation if the caller 13 | * wants to cache the full chunks, and make their own view of the data from 14 | * the chunks. 15 | * 16 | * @param {object} options read options 17 | * @param {AsyncBuffer} options.file file-like object containing parquet data 18 | * @param {FileMetaData} [options.metadata] parquet file metadata 19 | * @param {string[]} [options.columns] columns to read, all columns if undefined 20 | * @param {number} [options.rowStart] first requested row index (inclusive) 21 | * @param {number} [options.rowEnd] last requested row index (exclusive) 22 | * @param {Function} [options.onChunk] called when a column chunk is parsed. chunks may include row data outside the requested range. 23 | * @param {Function} [options.onComplete] called when all requested rows and columns are parsed 24 | * @param {Compressors} [options.compressor] custom decompressors 25 | * @returns {Promise} resolves when all requested rows and columns are parsed 26 | */ 27 | export function parquetRead(options: ParquetReadOptions): Promise 28 | 29 | /** 30 | * Read parquet metadata from an async buffer. 31 | * 32 | * An AsyncBuffer is like an ArrayBuffer, but the slices are loaded 33 | * asynchronously, possibly over the network. 34 | * 35 | * You must provide the byteLength of the buffer, typically from a HEAD request. 36 | * 37 | * In theory, you could use suffix-range requests to fetch the end of the file, 38 | * and save a round trip. But in practice, this doesn't work because chrome 39 | * deems suffix-range requests as a not-safe-listed header, and will require 40 | * a pre-flight. So the byteLength is required. 41 | * 42 | * To make this efficient, we initially request the last 512kb of the file, 43 | * which is likely to contain the metadata. If the metadata length exceeds the 44 | * initial fetch, 512kb, we request the rest of the metadata from the AsyncBuffer. 45 | * 46 | * This ensures that we either make one 512kb initial request for the metadata, 47 | * or a second request for up to the metadata size. 48 | * 49 | * @param {AsyncBuffer} asyncBuffer parquet file contents 50 | * @param {number} initialFetchSize initial fetch size in bytes (default 512kb) 51 | * @returns {Promise} parquet metadata object 52 | */ 53 | export function parquetMetadataAsync(asyncBuffer: AsyncBuffer, initialFetchSize?: number): Promise 54 | 55 | /** 56 | * Read parquet metadata from a buffer 57 | * 58 | * @param {ArrayBuffer} arrayBuffer parquet file contents 59 | * @returns {FileMetaData} parquet metadata object 60 | */ 61 | export function parquetMetadata(arrayBuffer: ArrayBuffer): FileMetaData 62 | 63 | /** 64 | * Return a tree of schema elements from parquet metadata. 65 | * 66 | * @param {FileMetaData} metadata parquet metadata object 67 | * @returns {SchemaTree} tree of schema elements 68 | */ 69 | export function parquetSchema(metadata: FileMetaData): SchemaTree 70 | 71 | /** 72 | * Decompress snappy data. 73 | * Accepts an output buffer to avoid allocating a new buffer for each call. 74 | * 75 | * @param {Uint8Array} input compressed data 76 | * @param {Uint8Array} output output buffer 77 | * @returns {boolean} true if successful 78 | */ 79 | export function snappyUncompress(input: Uint8Array, output: Uint8Array): boolean 80 | 81 | /** 82 | * Replace bigints with numbers. 83 | * When parsing parquet files, bigints are used to represent 64-bit integers. 84 | * However, JSON does not support bigints, so it's helpful to convert to numbers. 85 | * 86 | * @param {any} obj object to convert 87 | * @returns {unknown} converted object 88 | */ 89 | export function toJson(obj: any): any 90 | 91 | /** 92 | * Construct an AsyncBuffer for a URL. 93 | * 94 | * @param {string} url 95 | * @returns {Promise} 96 | */ 97 | export function asyncBufferFromUrl(url: string): Promise 98 | 99 | /** 100 | * Construct an AsyncBuffer for a local file using node fs package. 101 | * 102 | * @param {string} filename 103 | * @returns {Promise} 104 | */ 105 | export function asyncBufferFromFile(filename: string): Promise 106 | 107 | /** 108 | * Parquet query options for reading data 109 | */ 110 | export interface ParquetReadOptions { 111 | file: AsyncBuffer // file-like object containing parquet data 112 | metadata?: FileMetaData // parquet metadata, will be parsed if not provided 113 | columns?: string[] // columns to read, all columns if undefined 114 | rowStart?: number // inclusive 115 | rowEnd?: number // exclusive 116 | onChunk?: (chunk: ColumnData) => void // called when a column chunk is parsed. chunks may be outside the requested range. 117 | onComplete?: (rows: any[][]) => void // called when all requested rows and columns are parsed 118 | compressors?: Compressors // custom decompressors 119 | utf8?: boolean // decode byte arrays as utf8 strings (default true) 120 | } 121 | 122 | /** 123 | * A run of column data 124 | */ 125 | export interface ColumnData { 126 | columnName: string 127 | columnData: ArrayLike 128 | rowStart: number 129 | rowEnd: number 130 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./app/layout/App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById("root") as HTMLElement 8 | ); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/react-table-config.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UseColumnOrderInstanceProps, 3 | UseColumnOrderState, 4 | UseExpandedHooks, 5 | UseExpandedInstanceProps, 6 | UseExpandedOptions, 7 | UseExpandedRowProps, 8 | UseExpandedState, 9 | UseFiltersColumnOptions, 10 | UseFiltersColumnProps, 11 | UseFiltersInstanceProps, 12 | UseFiltersOptions, 13 | UseFiltersState, 14 | UseGlobalFiltersColumnOptions, 15 | UseGlobalFiltersInstanceProps, 16 | UseGlobalFiltersOptions, 17 | UseGlobalFiltersState, 18 | UseGroupByCellProps, 19 | UseGroupByColumnOptions, 20 | UseGroupByColumnProps, 21 | UseGroupByHooks, 22 | UseGroupByInstanceProps, 23 | UseGroupByOptions, 24 | UseGroupByRowProps, 25 | UseGroupByState, 26 | UsePaginationInstanceProps, 27 | UsePaginationOptions, 28 | UsePaginationState, 29 | UseResizeColumnsColumnOptions, 30 | UseResizeColumnsColumnProps, 31 | UseResizeColumnsOptions, 32 | UseResizeColumnsState, 33 | UseRowSelectHooks, 34 | UseRowSelectInstanceProps, 35 | UseRowSelectOptions, 36 | UseRowSelectRowProps, 37 | UseRowSelectState, 38 | UseRowStateCellProps, 39 | UseRowStateInstanceProps, 40 | UseRowStateOptions, 41 | UseRowStateRowProps, 42 | UseRowStateState, 43 | UseSortByColumnOptions, 44 | UseSortByColumnProps, 45 | UseSortByHooks, 46 | UseSortByInstanceProps, 47 | UseSortByOptions, 48 | UseSortByState 49 | } from 'react-table' 50 | 51 | declare module 'react-table' { 52 | // take this file as-is, or comment out the sections that don't apply to your plugin configuration 53 | 54 | export interface TableOptions> 55 | extends UseExpandedOptions, 56 | UseFiltersOptions, 57 | UseGlobalFiltersOptions, 58 | UseGroupByOptions, 59 | UsePaginationOptions, 60 | UseResizeColumnsOptions, 61 | UseRowSelectOptions, 62 | UseRowStateOptions, 63 | UseSortByOptions, 64 | // note that having Record here allows you to add anything to the options, this matches the spirit of the 65 | // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your 66 | // feature set, this is a safe default. 67 | Record {} 68 | 69 | export interface Hooks = Record> 70 | extends UseExpandedHooks, 71 | UseGroupByHooks, 72 | UseRowSelectHooks, 73 | UseSortByHooks {} 74 | 75 | export interface TableInstance = Record> 76 | extends UseColumnOrderInstanceProps, 77 | UseExpandedInstanceProps, 78 | UseFiltersInstanceProps, 79 | UseGlobalFiltersInstanceProps, 80 | UseGroupByInstanceProps, 81 | UsePaginationInstanceProps, 82 | UseRowSelectInstanceProps, 83 | UseRowStateInstanceProps, 84 | UseSortByInstanceProps {} 85 | 86 | export interface TableState = Record> 87 | extends UseColumnOrderState, 88 | UseExpandedState, 89 | UseFiltersState, 90 | UseGlobalFiltersState, 91 | UseGroupByState, 92 | UsePaginationState, 93 | UseResizeColumnsState, 94 | UseRowSelectState, 95 | UseRowStateState, 96 | UseSortByState {} 97 | 98 | export interface ColumnInterface = Record> 99 | extends UseFiltersColumnOptions, 100 | UseGlobalFiltersColumnOptions, 101 | UseGroupByColumnOptions, 102 | UseResizeColumnsColumnOptions, 103 | UseSortByColumnOptions {} 104 | 105 | export interface ColumnInstance = Record> 106 | extends UseFiltersColumnProps, 107 | UseGroupByColumnProps, 108 | UseResizeColumnsColumnProps, 109 | UseSortByColumnProps {} 110 | 111 | export interface Cell = Record, V = any> 112 | extends UseGroupByCellProps, 113 | UseRowStateCellProps {} 114 | 115 | export interface Row = Record> 116 | extends UseExpandedRowProps, 117 | UseGroupByRowProps, 118 | UseRowSelectRowProps, 119 | UseRowStateRowProps {} 120 | } -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------