├── .gitignore ├── LICENSE ├── README.md ├── backend ├── apiCore.py └── requirements.txt └── frontend ├── .idea ├── .gitignore ├── frontend.iml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLinters │ └── jshint.xml ├── modules.xml └── vcs.xml ├── README.md ├── demo.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | # customize 133 | .idea/ 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Reacubeth 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text2Graph 2 | Text2Graph model trained with T5 3 | 4 | Similar to [GraphGPT](https://github.com/varunshenoy/GraphGPT), this model converts unstructured natural language into a knowledge graph. We provide the trained T5 model for triple extraction. 5 | 6 | ![demo](frontend/demo.png) 7 | 8 | ## Model 9 | We select T5-base model for training with WebNLG dataset. The model file can be downloaded from [Huggingface](https://huggingface.co/Reacubeth/text2graph). 10 | 11 | 12 | ## Run 13 | 1. Backend 14 | 15 | Note that you should download our T5 model and put it into the backend folder. 16 | 17 | ```bash 18 | cd backend 19 | pip install -r requirements.txt 20 | python apiCore.py 21 | ``` 22 | 23 | 2. Frontend 24 | ```bash 25 | cd frontend 26 | npm install 27 | npm run start 28 | ``` 29 | -------------------------------------------------------------------------------- /backend/apiCore.py: -------------------------------------------------------------------------------- 1 | # Name: apiCore 2 | # Author: Reacubeth 3 | # Time: 2023/2/9 16:47 4 | # Mail: noverfitting@gmail.com 5 | # Site: www.omegaxyz.com 6 | # *_*coding:utf-8 *_* 7 | 8 | 9 | import uvicorn 10 | from fastapi import FastAPI, Query, Form, APIRouter, File, UploadFile 11 | from fastapi.middleware.cors import CORSMiddleware 12 | import time 13 | import os 14 | import torch 15 | from transformers import T5Tokenizer, T5ForConditionalGeneration 16 | import os 17 | import re 18 | 19 | 20 | GPU_ID = "0" 21 | os.environ["CUDA_VISIBLE_DEVICES"] = GPU_ID 22 | 23 | use_cuda = torch.cuda.is_available() 24 | if use_cuda: 25 | print('***************GPU_ID***************: ', GPU_ID) 26 | else: 27 | raise NotImplementedError 28 | 29 | # Model Definition 30 | t5_model_name = 't5-base' 31 | tokenizer = T5Tokenizer.from_pretrained(t5_model_name) 32 | kg_tokens_dict = ['', '', ''] 33 | num_added_toks = tokenizer.add_tokens(kg_tokens_dict) 34 | text_prefix = "TEXT: " 35 | graph_prefix = "GRAPH: " 36 | 37 | model = torch.load('webNLG_model.pkl') 38 | 39 | 40 | app = FastAPI( 41 | docs_url='/api/v1/docs', 42 | redoc_url='/api/v1/redoc', 43 | openapi_url='/api/v1/openapi.json' 44 | ) 45 | 46 | router = APIRouter() 47 | 48 | 49 | def parse_triple(content): 50 | entity_ls = set([_e.strip() for _e in list(set(re.findall(r"\s*([\s\w\.\/\-]+)[<$]*", content) + 51 | re.findall(r"\s*([\s\w\.\/\-]+)[<$]*", content)))]) 52 | 53 | hrt_ls = set([(_r[0].strip(), _r[1].strip(), _r[2].strip()) 54 | for _r in re.findall(r"([^<]+)([^<]+)([^<]+)", content)]) 55 | return entity_ls, hrt_ls 56 | 57 | 58 | def gen_json_response(hrt_ls): 59 | """ 60 | {"graph": { "nodes": [ { "id": 1, "label": "Bob", "color": "#ffffff" }, { "id": 2, "label": "Alice", "color": "#ff7675" } ], 61 | "edges": [ { "from": 1, "to": 2, "label": "roommate" }, ] } } 62 | """ 63 | graph = {"nodes": [], "edges": []} 64 | node_id = 0 65 | node_dict = {} 66 | for _h, _r, _t in hrt_ls: 67 | if _h not in node_dict: 68 | node_dict[_h] = node_id 69 | graph["nodes"].append({"id": node_id, "label": _h, "color": "#ffffff"}) 70 | node_id += 1 71 | if _t not in node_dict: 72 | node_dict[_t] = node_id 73 | graph["nodes"].append({"id": node_id, "label": _t, "color": "#ffffff"}) 74 | node_id += 1 75 | graph["edges"].append({"from": node_dict[_h], "to": node_dict[_t], "label": _r}) 76 | return {"graph": graph} 77 | 78 | 79 | @router.get('/get_graph') 80 | async def get_graph( 81 | text: str = Query(..., description='a sentence', example=''), 82 | ): 83 | start = time.time() 84 | input_content = text 85 | prefix = text_prefix 86 | 87 | input_content_tmp = tokenizer(prefix + input_content, return_tensors='pt', padding='max_length', max_length=500) 88 | input_ids = input_content_tmp.input_ids.cuda() 89 | am = input_content_tmp.attention_mask.cuda() 90 | 91 | model_outputs = model.generate(input_ids=input_ids, attention_mask=am, 92 | num_beams=4, length_penalty=2.0, max_length=500) 93 | 94 | out_content = tokenizer.decode(model_outputs[0], skip_special_tokens=True) 95 | if '' in out_content: 96 | entity_pool, hrt_pool = parse_triple(out_content) 97 | print('-----Graph-----') 98 | data = gen_json_response(hrt_pool) 99 | else: 100 | print(out_content) 101 | data = {"graph": {"nodes": [], "edges": []}} 102 | 103 | return {'time': time.time() - start, 'data': data} 104 | 105 | 106 | app.include_router(router) 107 | 108 | app.add_middleware( 109 | CORSMiddleware, 110 | allow_origins=["*"], 111 | allow_credentials=True, 112 | allow_methods=["*"], 113 | allow_headers=["*"], 114 | ) 115 | 116 | if __name__ == '__main__': 117 | uvicorn.run(app=app, host="127.0.0.1", port=8000, workers=1) 118 | 119 | # pip install python-multipart 120 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | python-multipart 2 | torch 3 | transformers==4.8.2 4 | uvicorn 5 | fastapi -------------------------------------------------------------------------------- /frontend/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /frontend/.idea/frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /frontend/.idea/jsLinters/jshint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | -------------------------------------------------------------------------------- /frontend/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Text2Gaph-T5 2 | ### Natural Language → Knowledge Graph 3 | 4 | ![demo](demo.png) 5 | 6 | ## Setup 7 | 8 | 1. Run `npm install` to download required dependencies (currently just [react-graph-vis](https://github.com/crubier/react-graph-vis)). 9 | 2. Run `npm run start`. GraphGPT should open up in a new browser tab. 10 | -------------------------------------------------------------------------------- /frontend/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyjigsaw/Text2Graph/8593f681c3aca0898c6fce8f0aff3b485a0b155b/frontend/demo.png -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphgpt", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-graph-vis": "^1.0.7", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyjigsaw/Text2Graph/8593f681c3aca0898c6fce8f0aff3b485a0b155b/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | GraphGPT 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyjigsaw/Text2Graph/8593f681c3aca0898c6fce8f0aff3b485a0b155b/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyjigsaw/Text2Graph/8593f681c3aca0898c6fce8f0aff3b485a0b155b/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "GraphGPT", 3 | "name": "GraphGPT", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noticia+Text:wght@400;700&display=swap'); 2 | 3 | .headerText { 4 | font-family: 'Noticia Text'; 5 | font-weight: 900; 6 | font-size: 60px; 7 | text-align: center; 8 | padding-top: 40px; 9 | } 10 | 11 | .subheaderText { 12 | font-family: 'Noticia Text'; 13 | font-weight: 400; 14 | font-size: 20px; 15 | text-align: center; 16 | width: 60%; 17 | margin: 0 auto; 18 | } 19 | 20 | .opensourceText { 21 | font-family: 'Noticia Text'; 22 | font-weight: 400; 23 | font-size: 14px; 24 | text-align: center; 25 | width: 60%; 26 | margin: 10px auto; 27 | } 28 | 29 | .container { 30 | padding-right: 15px; 31 | padding-left: 15px; 32 | margin-right: auto; 33 | margin-left: auto; 34 | } 35 | 36 | @media (min-width: 768px) { 37 | .container { 38 | width: 750px; 39 | } 40 | } 41 | 42 | @media (min-width: 992px) { 43 | .container { 44 | width: 970px; 45 | } 46 | } 47 | 48 | @media (min-width: 1200px) { 49 | .container { 50 | width: 1170px; 51 | } 52 | } 53 | 54 | .graphContainer { 55 | width: 80%; 56 | height: 80%; 57 | background-color: rgb(255, 255, 255); 58 | border-radius: 10px; 59 | overflow: hidden; 60 | border: 1px solid rgb(0, 0, 0); 61 | box-shadow: 0 0 5px rgb(0 0 0 / 10%); 62 | margin: 0 auto; 63 | margin-bottom: 10px; 64 | } 65 | 66 | .footer { 67 | font-family: 'Noticia Text'; 68 | font-weight: 400; 69 | font-size: 13px; 70 | text-align: center; 71 | margin: 0 auto; 72 | margin-bottom: 100px; 73 | } 74 | 75 | .inputContainer { 76 | width: 60%; 77 | margin: 0 auto; 78 | margin-top: 50px; 79 | margin-bottom: 50px; 80 | } 81 | 82 | .searchBar { 83 | border: 1px solid rgb(0, 0, 0); 84 | border-radius: 5px; 85 | box-shadow: 0 0 5px rgb(0 0 0 / 10%); 86 | display: block; 87 | font-size: 16px; 88 | height: 50px; 89 | outline: none; 90 | padding: 0 10px; 91 | width: 90%; 92 | margin-left: auto; 93 | margin-right: auto; 94 | margin-bottom: 20px; 95 | } 96 | 97 | .apiKeyTextField { 98 | border: 1px solid rgb(0, 0, 0); 99 | border-radius: 5px; 100 | box-shadow: 0 0 5px rgb(0 0 0 / 10%); 101 | display: block; 102 | font-size: 16px; 103 | height: 50px; 104 | outline: none; 105 | padding: 0 10px; 106 | width: 90%; 107 | margin-left: auto; 108 | margin-right: auto; 109 | margin-bottom: 20px; 110 | } 111 | 112 | .generateButton { 113 | background-color: rgb(0, 0, 0); 114 | border: none; 115 | border-radius: 5px; 116 | color: rgb(255, 255, 255); 117 | cursor: pointer; 118 | font-size: 16px; 119 | height: 50px; 120 | outline: none; 121 | width: 40%; 122 | display: inline-block; 123 | margin-right: 5px; 124 | } 125 | 126 | .generateButton:hover { 127 | background-color: rgb(0, 0, 0, 0.8); 128 | } 129 | 130 | .generateButton:disabled { 131 | background-color: rgb(0, 0, 0, 0.5); 132 | cursor: wait; 133 | } 134 | 135 | .clearButton { 136 | background-color: rgb(255, 255, 255); 137 | border: 1px solid rgb(0, 0, 0); 138 | border-radius: 5px; 139 | color: rgb(0, 0, 0); 140 | cursor: pointer; 141 | font-size: 16px; 142 | height: 50px; 143 | outline: none; 144 | width: 40%; 145 | margin-left: auto; 146 | margin-right: auto; 147 | display: inline-block; 148 | margin-left: 5px; 149 | } 150 | 151 | .clearButton:hover { 152 | background-color: rgba(239, 239, 239, 0.8); 153 | } -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import Graph from "react-graph-vis"; 3 | import React, { useState } from "react"; 4 | const options = { 5 | layout: { 6 | hierarchical: false 7 | }, 8 | edges: { 9 | color: "#34495e" 10 | } 11 | }; 12 | 13 | function App() { 14 | const [state, setState] = useState( 15 | { 16 | counter: 0, 17 | graph: { 18 | nodes: [], 19 | edges: [] 20 | } 21 | }) 22 | const { graph } = state; 23 | 24 | const clearState = () => { 25 | setState({ 26 | counter: 0, 27 | graph: { 28 | nodes: [], 29 | edges: [] 30 | } 31 | }) 32 | } 33 | 34 | const queryPrompt = (prompt) => { 35 | console.log(prompt) 36 | 37 | fetch('http://127.0.0.1:8000/get_graph?text=' + prompt) 38 | .then(response => { 39 | if (!response.ok) { 40 | throw new Error('Something went wrong with the request, please check the Network log'); 41 | } 42 | return response.json(); 43 | }) 44 | .then((response) => { 45 | const text = response.data; 46 | console.log(text); 47 | const new_graph = text; 48 | console.log(new_graph); 49 | setState(new_graph, () => { 50 | console.log(state); 51 | }); 52 | document.body.style.cursor = 'default'; 53 | document.getElementsByClassName("generateButton")[0].disabled = false; 54 | document.getElementsByClassName("searchBar")[0].value = ""; 55 | }).catch((error) => { 56 | console.log(error); 57 | alert(error); 58 | document.body.style.cursor = 'default'; 59 | document.getElementsByClassName("generateButton")[0].disabled = false; 60 | }); 61 | } 62 | 63 | 64 | const createGraph = () => { 65 | document.body.style.cursor = 'wait'; 66 | 67 | document.getElementsByClassName("generateButton")[0].disabled = true; 68 | const prompt = document.getElementsByClassName("searchBar")[0].value; 69 | 70 | queryPrompt(prompt); 71 | } 72 | 73 | return ( 74 |
75 |

Text2Graph-T5 🔎

76 |

Build complex, directed graphs to add structure to your ideas using natural language. Understand the relationships between people, systems, and maybe solve a mystery.

77 |
78 |
79 | 80 | 81 | 82 |
83 |
84 |
85 | 86 |
87 |

Pro tip: don't take a screenshot! You can right-click and save the graph as a .png 📸

88 |
89 | ); 90 | } 91 | 92 | export default App; 93 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | color: black; 9 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | ); 11 | 12 | // If you want to start measuring performance in your app, pass a function 13 | // to log results (for example: reportWebVitals(console.log)) 14 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 15 | reportWebVitals(); 16 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 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 | --------------------------------------------------------------------------------