├── .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 | 
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 | 
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 | You need to enable JavaScript to run this app.
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 | handleSearch("local")}
135 | disabled={
136 | !serverUp ||
137 | !localSearchEnabled ||
138 | loadingLocal ||
139 | loadingGlobal
140 | }
141 | >
142 | {loadingLocal ? : "Local Search"}
143 |
144 | handleSearch("global")}
149 | disabled={
150 | !serverUp ||
151 | !globalSearchEnabled ||
152 | loadingLocal ||
153 | loadingGlobal
154 | }
155 | >
156 | {loadingGlobal ? : "Global Search"}
157 |
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 | }
565 | >
566 | Search Nodes/Links
567 |
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 | }
888 | >
889 | Ask Query (Local/Global Search)
890 |
891 | }
895 | color="warning"
896 | disabled={apiSearchResults === null}
897 | >
898 | Clear Query Results
899 |
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 |
102 | handleFocusButtonClick(node as CustomNode)
103 | }
104 | >
105 | Focus
106 |
107 |
109 | handleNodeClick(node as CustomNode)
110 | }
111 | sx={{ marginLeft: 1 }}
112 | >
113 | Details
114 |
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 |
162 | handleFocusLinkClick(link as CustomLink)
163 | }
164 | >
165 | Focus
166 |
167 |
169 | handleLinkClick(link as CustomLink)
170 | }
171 | sx={{ marginLeft: 1 }}
172 | >
173 | Details
174 |
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 |
--------------------------------------------------------------------------------