├── 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 |
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 |
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 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 |
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 |
380 |
381 |
382 |
383 | );
384 | }
385 |
386 |
387 |
--------------------------------------------------------------------------------