├── src ├── vite-env.d.ts ├── assets │ └── Onest-Regular.ttf ├── main.tsx ├── constants.ts ├── App.css ├── toggleTheme.ts ├── index.css ├── StatusBar.tsx ├── Footer.tsx └── App.tsx ├── postcss.config.js ├── requirements.txt ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── .eslintrc.cjs ├── index.html ├── tailwind.config.js ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── main.py /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/assets/Onest-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noahgsolomon/Graphzila/HEAD/src/assets/Onest-Regular.ttf -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | graphviz==0.20.1 3 | networkx==3.1 4 | openai==0.28.0 5 | beautifulsoup4==4.12.2 6 | neo4j==5.12.0 7 | python-dotenv==1.0.0 8 | requests~=2.31.0 -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | const LOCALSTORAGE_REMOVE_GITHUB_KEY = "removeGithub"; 2 | const LOCALSTORAGE_THEME_KEY = "theme"; 3 | 4 | const DARK_THEME_KEY = "dark"; 5 | const LIGHT_THEME_KEY = "light"; 6 | 7 | export { 8 | LOCALSTORAGE_REMOVE_GITHUB_KEY, 9 | LOCALSTORAGE_THEME_KEY, 10 | DARK_THEME_KEY, 11 | LIGHT_THEME_KEY, 12 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Graphzila 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import forms from "@tailwindcss/forms"; 2 | 3 | import typography from "@tailwindcss/typography"; 4 | 5 | /** @type {import('tailwindcss').Config} */ 6 | export default { 7 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 8 | darkMode: 'class', 9 | theme: { 10 | extend: { 11 | boxShadow: { 12 | custom: '4px 4px #0b0b0b', 13 | customHover: '7px 7px #0b0b0b', 14 | }, 15 | fontFamily: { 16 | custom: ['Onest-Regular', 'sans-serif'], 17 | } 18 | }, 19 | }, 20 | plugins: [ 21 | forms, 22 | typography, 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* Use Tailwind */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | 7 | @font-face { 8 | font-family: "Onest-Regular"; 9 | src: url("./assets/Onest-Regular.ttf") format("truetype"); 10 | } 11 | 12 | @keyframes fire { 13 | 0% { 14 | stroke: red; 15 | } 16 | 25% { 17 | stroke: orange; 18 | } 19 | 50% { 20 | stroke: yellow; 21 | } 22 | 75% { 23 | stroke: orange; 24 | } 25 | 100% { 26 | stroke: red; 27 | } 28 | } 29 | 30 | .fire-stroke { 31 | animation: fire 3s linear infinite; 32 | } 33 | 34 | body { 35 | @apply text-lg font-medium leading-loose; 36 | } 37 | 38 | body.dark { 39 | @apply bg-[#1a1a1a] 40 | text-gray-200; 41 | } 42 | 43 | body.light { 44 | @apply bg-gray-50 45 | text-gray-800; 46 | } -------------------------------------------------------------------------------- /src/toggleTheme.ts: -------------------------------------------------------------------------------- 1 | import {DARK_THEME_KEY, LIGHT_THEME_KEY, LOCALSTORAGE_THEME_KEY} from "./constants.ts"; 2 | 3 | export const toggleTheme = () => { 4 | if (document.body.classList.contains(DARK_THEME_KEY)) { 5 | document.body.classList.remove(DARK_THEME_KEY); 6 | document.body.classList.add(LIGHT_THEME_KEY); 7 | window.localStorage.setItem(LOCALSTORAGE_THEME_KEY, LIGHT_THEME_KEY); 8 | const themeChangedEvent = new Event("themeChanged"); 9 | window.dispatchEvent(themeChangedEvent); 10 | } else { 11 | document.body.classList.add(DARK_THEME_KEY); 12 | document.body.classList.remove(LIGHT_THEME_KEY); 13 | window.localStorage.setItem(LOCALSTORAGE_THEME_KEY, DARK_THEME_KEY); 14 | const themeChangedEvent = new Event("themeChanged"); 15 | window.dispatchEvent(themeChangedEvent); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Noah Solomon 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphv2", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/aspect-ratio": "^0.4.2", 14 | "@tailwindcss/forms": "^0.5.6", 15 | "@tailwindcss/typography": "^0.5.10", 16 | "cytoscape": "^3.26.0", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-icons": "^4.11.0", 20 | "react-router-dom": "^6.16.0", 21 | "react-spring": "^9.7.2" 22 | }, 23 | "devDependencies": { 24 | "@types/cytoscape": "^3.19.11", 25 | "@types/react": "^18.2.15", 26 | "@types/react-dom": "^18.2.7", 27 | "@typescript-eslint/eslint-plugin": "^6.0.0", 28 | "@typescript-eslint/parser": "^6.0.0", 29 | "@vitejs/plugin-react-swc": "^3.3.2", 30 | "autoprefixer": "^10.4.15", 31 | "eslint": "^8.45.0", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.4.3", 34 | "postcss": "^8.4.29", 35 | "tailwindcss": "^3.3.3", 36 | "typescript": "^5.0.2", 37 | "vite": "^4.4.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/StatusBar.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import { useSpring, animated } from "react-spring"; 3 | 4 | const StatusBar: FC<{ 5 | message: string; 6 | color: string; 7 | }> = ({ message, color }) => { 8 | const [show, setShow] = useState(false); 9 | const [animateDown, setAnimateDown] = useState(false); 10 | const [animateUp, setAnimateUp] = useState(false); 11 | 12 | const animate = useSpring({ 13 | opacity: show ? 1 : 0, 14 | top: animateUp ? "-100px" : animateDown ? "50px" : "-100px", 15 | }); 16 | 17 | useEffect(() => { 18 | setShow(true); 19 | setAnimateDown(true); 20 | const timer = setTimeout(() => { 21 | setAnimateUp(true); 22 | }, 2000); 23 | return () => { 24 | clearTimeout(timer); 25 | }; 26 | }, []); 27 | 28 | useEffect(() => { 29 | if (animateUp) { 30 | const timer = setTimeout(() => { 31 | setShow(false); 32 | }, 1000); 33 | return () => { 34 | clearTimeout(timer); 35 | }; 36 | } 37 | }, [animateUp]); 38 | 39 | return ( 40 | 44 | {message} 45 | 46 | ); 47 | }; 48 | 49 | export default StatusBar; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 🐉 Knowledge Graph Generator 🐉 3 |

4 | 5 |

6 | 7 | graphzila-dark 8 | 9 |

10 | 11 |

12 | Transform text into epic knowledge graphs with Graphzila! 📚🔮 13 |

14 | 15 |

16 | Explore Graphzila | 17 | Star the Repository ⭐ 18 |

19 | 20 | --- 21 | 22 | ## 🌟 Features 23 | 24 | - Harness the power of OpenAI's GPT-3.5 Turbo. 25 | - Create detailed knowledge graphs from text descriptions. 26 | - Customize node and edge attributes like colors and Wikipedia links. 27 | - Unleash the dragon of knowledge within you! 🐉✨ 28 | 29 | ## 🚀 How to Use 30 | 31 | 1. Visit [Graphzila.com](https://graphzila.com). 32 | 2. Enter a text description related to the topic you want to explore. 33 | 3. Witness the magic as it generates your knowledge graph! 34 | 35 | ## 🐲 Support Graphzila 36 | 37 | If you find Graphzila helpful and want to support this mystical project: 38 | 39 | - 🌟 **Star the GitHub repository**: [graphzila](https://github.com/noahgsolomon/graphzila). 40 | - Share the tool with fellow adventurers on their quest for knowledge. 41 | - Provide feedback and suggestions to help us improve. 42 | 43 | Let's journey through the realms of knowledge together! 🌐🗺️ 44 | 45 |
46 |

47 | graphzila-light 48 |

49 |
50 | 51 | ⭐ **Thank you for using Graphzila!** ⭐ 52 | -------------------------------------------------------------------------------- /src/Footer.tsx: -------------------------------------------------------------------------------- 1 | 2 | const Footer = () => { 3 | return ( 4 | 104 | ); 105 | }; 106 | 107 | export default Footer; -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import openai 4 | import logging 5 | from bs4 import BeautifulSoup 6 | import re 7 | from neo4j import GraphDatabase 8 | 9 | # Set your OpenAI API key 10 | openai.api_key = "" 11 | response_data = "" 12 | 13 | # If Neo4j credentials are set, then Neo4j is used to store information 14 | neo4j_username = os.environ.get("NEO4J_USERNAME") 15 | neo4j_password = os.environ.get("NEO4J_PASSWORD") 16 | neo4j_url = os.environ.get("NEO4J_URL") 17 | neo4j_driver = None 18 | if neo4j_username and neo4j_password and neo4j_url: 19 | neo4j_driver = GraphDatabase.driver( 20 | neo4j_url, auth=(neo4j_username, neo4j_password)) 21 | 22 | # Configure logging 23 | logging.basicConfig(level=logging.DEBUG) 24 | logger = logging.getLogger() 25 | 26 | # Define your AWS Lambda handler function 27 | def lambda_handler(event, context): 28 | print("hiii squidward") 29 | try: 30 | global response_data 31 | request_data = json.loads(event['body']) 32 | print("request: " + str(request_data)) 33 | 34 | user_input = request_data.get("user_input", "") 35 | 36 | if not user_input: 37 | return { 38 | "statusCode": 400, 39 | "body": json.dumps({"error": "No input provided"}) 40 | } 41 | 42 | print("Starting OpenAI call") 43 | completion = openai.ChatCompletion.create( 44 | model="gpt-3.5-turbo-16k", 45 | messages=[ 46 | { 47 | "role": "user", 48 | "content": f"Help me understand following by describing as a detailed knowledge graph: {user_input}" 49 | } 50 | ], 51 | functions=[ 52 | { 53 | "name": "knowledge_graph", 54 | "description": "Generate a knowledge graph with entities and relationships. Make the label the relationship between the nodes like java to jre would be 'runs on', etc.. Use the colors to help " 55 | "differentiate between different node or edge types/categories to differentiate between nodes. Always provide light " 56 | "pastel colors that work well with black font. Please try to use a different color for different nodes. And if you can find a wiki for the " 57 | "concept, share the full link, empty string otherwise.", 58 | "parameters": { 59 | "type": "object", 60 | "properties": { 61 | "metadata": { 62 | "type": "object", 63 | "properties": { 64 | "createdDate": {"type": "string"}, 65 | "lastUpdated": {"type": "string"}, 66 | "description": {"type": "string"} 67 | } 68 | }, 69 | "nodes": { 70 | "type": "array", 71 | "items": { 72 | "type": "object", 73 | "properties": { 74 | "id": {"type": "string"}, 75 | "label": {"type": "string"}, 76 | "type": {"type": "string"}, 77 | "color": {"type": "string"}, # Added color property 78 | "wiki": {"type": "string"}, # Added wiki property 79 | "properties": { 80 | "type": "object", 81 | "description": "Additional attributes for the node" 82 | } 83 | }, 84 | "required": [ 85 | "id", 86 | "label", 87 | "type", 88 | "color", 89 | "wiki" 90 | ] # Added color to required 91 | } 92 | }, 93 | "edges": { 94 | "type": "array", 95 | "items": { 96 | "type": "object", 97 | "properties": { 98 | "from": {"type": "string"}, 99 | "to": {"type": "string"}, 100 | "relationship": {"type": "string"}, 101 | "direction": {"type": "string"}, 102 | "color": {"type": "string"}, # Added color property 103 | "properties": { 104 | "type": "object", 105 | "description": "Additional attributes for the edge" 106 | } 107 | }, 108 | "required": [ 109 | "from", 110 | "to", 111 | "relationship", 112 | "color" 113 | ] # Added color to required 114 | } 115 | } 116 | }, 117 | "required": ["nodes", "edges"] 118 | } 119 | } 120 | ], 121 | function_call={"name": "knowledge_graph"} 122 | ) 123 | 124 | response_data = completion.choices[0]["message"]["function_call"]["arguments"] 125 | # Remove trailing commas 126 | response_data = re.sub(r',\s*}', '}', response_data) 127 | response_data = re.sub(r',\s*]', ']', response_data) 128 | print(response_data) 129 | 130 | # Process graph data using the response_data 131 | if neo4j_driver: 132 | nodes, _, _ = neo4j_driver.execute_query(""" 133 | MATCH (n) 134 | WITH collect( 135 | {data: {id: n.id, label: n.label, color: n.color, wiki: n.wiki}}) AS node 136 | RETURN node 137 | """) 138 | 139 | print() 140 | nodes = [el['node'] for el in nodes][0] 141 | 142 | edges, _, _ = neo4j_driver.execute_query(""" 143 | MATCH (s)-[r]->(t) 144 | WITH collect( 145 | {data: {source: s.id, target: t.id, label:r.type, color: r.color, wiki: r.wiki}} 146 | ) AS rel 147 | RETURN rel 148 | """) 149 | edges = [el['rel'] for el in edges][0] 150 | else: 151 | print(response_data) 152 | response_dict = json.loads(response_data) 153 | # Assume response_data is global or passed appropriately 154 | nodes = [ 155 | { 156 | "data": { 157 | "id": node["id"], 158 | "label": node["label"], 159 | "color": node.get("color", "defaultColor"), 160 | "wiki": node.get("wiki", ""), 161 | } 162 | } 163 | for node in response_dict["nodes"] 164 | ] 165 | edges = [ 166 | { 167 | "data": { 168 | "source": edge["from"], 169 | "target": edge["to"], 170 | "label": edge["relationship"], 171 | "color": edge.get("color", "defaultColor"), 172 | } 173 | } 174 | for edge in response_dict["edges"] 175 | ] 176 | 177 | return { 178 | "statusCode": 200, 179 | "body": json.dumps({"elements": {"nodes": nodes, "edges": edges}}) 180 | } 181 | except Exception as e: 182 | logger.error(str(e)) 183 | return { 184 | "statusCode": 500, 185 | "body": json.dumps({"error": "Internal server error"}) 186 | } 187 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import {useState, FormEvent, useEffect} from 'react'; 2 | import "./App.css"; 3 | import cytoscape, { ElementsDefinition, Stylesheet } from 'cytoscape'; 4 | import {DARK_THEME_KEY, LIGHT_THEME_KEY, LOCALSTORAGE_THEME_KEY} from "./constants.ts"; 5 | import {toggleTheme} from "./toggleTheme.ts"; 6 | import Footer from "./Footer.tsx"; 7 | import StatusBar from "./StatusBar.tsx"; 8 | 9 | 10 | interface Metadata { 11 | createdDate: string; 12 | lastUpdated: string; 13 | description: string; 14 | } 15 | 16 | interface Node { 17 | id: string; 18 | label: string; 19 | type: string; 20 | color: string; 21 | wiki: string; 22 | } 23 | 24 | interface Edge { 25 | from: string; 26 | to: string; 27 | relationship: string; 28 | direction: string; 29 | color: string; 30 | } 31 | 32 | interface ResponseData { 33 | metadata: Metadata; 34 | nodes: Node[]; 35 | edges: Edge[]; 36 | } 37 | 38 | export default function App() { 39 | const [isLoading, setIsLoading] = useState(false); 40 | const [userInput, setUserInput] = useState(''); 41 | const [generated, setGenerated] = useState(false); 42 | const [error, setError] = useState(false); 43 | const [theme, setTheme] = useState< 44 | typeof LIGHT_THEME_KEY | typeof DARK_THEME_KEY 45 | >( 46 | (localStorage.getItem(LOCALSTORAGE_THEME_KEY) as 47 | | typeof LIGHT_THEME_KEY 48 | | typeof DARK_THEME_KEY) || LIGHT_THEME_KEY 49 | ); 50 | 51 | useEffect(() => { 52 | const userTheme = window.localStorage.getItem(LOCALSTORAGE_THEME_KEY); 53 | const systemTheme = window.matchMedia( 54 | `(prefers-color-scheme:${DARK_THEME_KEY})` 55 | ).matches 56 | ? DARK_THEME_KEY 57 | : LIGHT_THEME_KEY; 58 | 59 | if (!userTheme) { 60 | document.body.classList.add(systemTheme); 61 | localStorage.setItem(LOCALSTORAGE_THEME_KEY, systemTheme); 62 | window.location.reload(); 63 | } else { 64 | document.body.classList.add(userTheme); 65 | } 66 | }, []); 67 | 68 | const postData = async (url: string, data: object): Promise => { 69 | const response = await fetch(url, { 70 | method: 'POST', 71 | headers: { 72 | 'Content-Type': 'application/json', 73 | }, 74 | body: JSON.stringify(data), 75 | }); 76 | 77 | if (!response.ok) { 78 | throw new Error(await response.text()); 79 | } 80 | 81 | const responseJson = await response.json() 82 | 83 | return await responseJson; 84 | }; 85 | 86 | interface ServerResponse { 87 | elements: { 88 | nodes: Array<{ data: { id: string; label: string, wiki: string, color: string } }>; 89 | edges: Array<{ data: { source: string; target: string, label: string } }>; 90 | }; 91 | } 92 | 93 | // Function to parse the server response to a Cytoscape-compatible object 94 | const parseResponseToElements = (graphData: ServerResponse) => { 95 | const { nodes, edges } = graphData.elements; 96 | const elements: ElementsDefinition = { nodes: [], edges: [] }; 97 | 98 | nodes.forEach((node) => { 99 | elements.nodes.push({ 100 | data: { 101 | icon: 'https://img.icons8.com/emoji/150/fire.png', 102 | id: node.data.id, 103 | label: node.data.label, 104 | gradient: node.data.color, 105 | wiki: node.data.wiki 106 | } 107 | }); 108 | }); 109 | 110 | edges.forEach((edge) => { 111 | elements.edges.push({ 112 | data: { 113 | source: edge.data.source, 114 | target: edge.data.target, 115 | label: edge.data.label 116 | } 117 | }); 118 | }); 119 | 120 | return elements; 121 | }; 122 | 123 | const createGraph = (data: ElementsDefinition) => { 124 | const cy = cytoscape({ 125 | container: document.getElementById('cy') as HTMLElement, 126 | elements: data, 127 | style: [ 128 | { 129 | selector: 'node', 130 | style: { 131 | 'background-image': 'data(icon)', 132 | 'background-color': 'data(gradient)', 133 | 'label': 'data(label)', 134 | 'font-size': '10px', 135 | 'font-family': 'Onest-Regular, Arial, sans-serif', 136 | 'background-fit': 'contain', 137 | 'width': '30px', 138 | 'height': '30px', 139 | 'color': theme === LIGHT_THEME_KEY ? '#2D3748' : '#F9FAFB', 140 | 'border-width': '0.5px', 141 | 'border-color': '#ccc', 142 | } 143 | }, 144 | { 145 | selector: 'node.root', 146 | style: { 147 | 'background-image': 'https://img.icons8.com/stickers/150/dragon.png', 148 | } 149 | }, 150 | { 151 | selector: 'edge', 152 | style: { 153 | 'curve-style': 'unbundled-bezier', 154 | 'width': 1, 155 | 'line-color': '#ccc', 156 | 'target-arrow-color': '#ccc', 157 | 'target-arrow-shape': 'triangle', 158 | 'font-size': '8px', 159 | 'font-family': 'Onest-Regular, Arial, sans-serif', 160 | 'color': theme === LIGHT_THEME_KEY ? '#2D3748' : '#F9FAFB', 161 | 'label': 'data(label)', 162 | 'text-opacity': 0.8 163 | } 164 | } 165 | ] as Stylesheet[], 166 | layout: { 167 | name: 'cose' 168 | } 169 | }); 170 | 171 | cy.nodes().forEach((node) => { 172 | const wikiData = node.data('wiki'); 173 | if (wikiData !== "") { 174 | node.style({ 175 | 'border-color': '#FFD700', 176 | 'border-width': '1px' 177 | }); 178 | } 179 | }); 180 | 181 | cy.on('mouseover', 'node', function(evt){ 182 | const node = evt.target; 183 | const wikiData = node.data('wiki'); 184 | if (wikiData !== "") { 185 | node.animate({ 186 | style: { 'border-width': '2px' }, 187 | }, { 188 | duration: 100 189 | }); 190 | } 191 | }); 192 | 193 | cy.on('mouseout', 'node', function(evt){ 194 | const node = evt.target; 195 | const wikiData = node.data('wiki'); 196 | if (wikiData !== "") { 197 | node.animate({ 198 | style: { 'border-width': '1px' }, 199 | }, { 200 | duration: 100 201 | }); 202 | } 203 | }); 204 | 205 | cy.on('tap', 'node', function(evt){ 206 | const nodeData = evt.target.data(); 207 | if (nodeData.wiki) { 208 | window.open(nodeData.wiki, '_blank'); 209 | } 210 | }); 211 | const rootNode = cy.nodes().eq(0); 212 | rootNode.addClass('root'); 213 | }; 214 | 215 | const handleSubmit = async (e: FormEvent) => { 216 | e.preventDefault(); 217 | setGenerated(false); 218 | setIsLoading(true); 219 | 220 | try { 221 | const graphData = await postData('', { user_input: userInput }); 222 | setIsLoading(false); 223 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 224 | // @ts-ignore 225 | const cytoData = parseResponseToElements(graphData); 226 | setTimeout(() => { 227 | createGraph(cytoData); 228 | }, 100); 229 | setGenerated(true); 230 | } catch (error) { 231 | setIsLoading(false); 232 | setError(true); 233 | setTimeout(() => { 234 | setError(false); 235 | }, 3000); 236 | console.error('Fetch Error:', error); 237 | } 238 | }; 239 | 240 | return ( 241 |
242 | 251 |
254 |
255 |
256 | {'graphzila'}/

Graphzila

Powered by OpenAI

257 |
258 |
{ 260 | toggleTheme(); 261 | setTheme( 262 | (localStorage.getItem(LOCALSTORAGE_THEME_KEY) as 263 | | typeof LIGHT_THEME_KEY 264 | | typeof DARK_THEME_KEY) || typeof LIGHT_THEME_KEY 265 | ); 266 | }} 267 | > 268 | {theme === LIGHT_THEME_KEY ? ( 269 | 274 | 275 | 276 | ) : ( 277 | 282 | 287 | 288 | 289 | )} 290 |
291 |
292 |
293 |
294 |
295 |
296 | 299 |

300 | Enter a keyword or topic to awaken your dragon's wisdom. The mystical graph will reveal interconnected knowledge and secrets. 301 |

302 |
303 | setUserInput(e.target.value)} 310 | value={userInput} 311 | required 312 | /> 313 | 319 |
320 |
321 | {`${userInput.length}/75`} 322 |
323 | {generated && ( 324 |

325 | Click on nodes with gold borders to open their associated links. 326 |

327 | )} 328 |
329 | {!isLoading ? 330 | ( 331 |
332 | ) : ( 333 |
334 |
335 |
336 |
337 | 341 | 353 | 358 | 359 |
360 | Loading 361 |

This might take 1-2 minutes...

362 |
363 |
364 |
365 | ) 366 | 367 | } 368 |
369 |
370 | {isLoading && ( 371 | 372 | )} 373 | {generated && ( 374 | 375 | )} 376 | {error && ( 377 | 378 | )} 379 |
381 | 382 | 383 | ); 384 | } 385 | 386 | 387 | --------------------------------------------------------------------------------