├── package.json ├── wsgi.py ├── requirements.txt ├── vercel.json ├── app.py └── templates └── index.html /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": "18.x" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | if __name__ == "__main__": 4 | app.run() 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.0 2 | google-generativeai==0.4.1 3 | python-docx==1.0.0 4 | PyPDF2==3.0.1 5 | werkzeug==3.0.1 6 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "wsgi.py", 6 | "use": "@vercel/python" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "wsgi.py" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, jsonify 2 | import logging 3 | import google.generativeai as genai 4 | from google.generativeai.types import GenerationConfig 5 | import re 6 | import json 7 | import os 8 | from werkzeug.utils import secure_filename 9 | from docx import Document 10 | import PyPDF2 11 | import tempfile 12 | 13 | # Replace with your actual API key 14 | api_key = os.environ.get("API_KEY") # Replace this with your API key 15 | 16 | app = Flask(__name__) 17 | 18 | # Configure the Google Generative AI API 19 | genai.configure(api_key=api_key) 20 | 21 | # Generation configurations 22 | generation_config = GenerationConfig( 23 | temperature=0.9, 24 | top_p=1, 25 | top_k=1, 26 | max_output_tokens=2048, 27 | candidate_count=1 28 | ) 29 | 30 | # Initialize the model for flowchart generation 31 | model = genai.GenerativeModel('gemini-1.5-flash') 32 | 33 | # Configure upload folder and allowed extensions 34 | app.config['UPLOAD_FOLDER'] = 'uploads' 35 | app.config['ALLOWED_EXTENSIONS'] = {'pdf', 'docx'} 36 | 37 | # In-memory storage for the current flowchart data (replace with a database for persistence) 38 | current_flowchart_data = {"nodes": [], "edges": []} 39 | is_chart_modifying = False # flag to prevent concurrent modifications 40 | 41 | def allowed_file(filename): 42 | return '.' in filename and \ 43 | filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'] 44 | 45 | def extract_text_from_docx(file_path): 46 | doc = Document(file_path) 47 | full_text = [] 48 | for para in doc.paragraphs: 49 | if para.text.strip(): # Only include non-empty paragraphs 50 | full_text.append(para.text) 51 | return '\n'.join(full_text) 52 | 53 | def extract_text_from_pdf(file_path): 54 | with open(file_path, 'rb') as f: 55 | pdfReader = PyPDF2.PdfReader(f) 56 | full_text = [] 57 | for page in pdfReader.pages: 58 | text = page.extract_text() 59 | if text.strip(): # Only include non-empty pages 60 | full_text.append(text) 61 | return '\n'.join(full_text) 62 | 63 | def clean_and_validate_json(text): 64 | """Clean and validate JSON from the model's response.""" 65 | json_match = re.search(r'\{[\s\S]*\}', text) 66 | if not json_match: 67 | return None 68 | 69 | json_str = json_match.group() 70 | 71 | json_str = re.sub(r'```json\s*', '', json_str) 72 | json_str = re.sub(r'```\s*$', '', json_str) 73 | json_str = json_str.strip() 74 | 75 | try: 76 | json_data = json.loads(json_str) 77 | 78 | if not all(key in json_data for key in ['nodes', 'edges']): 79 | return None 80 | 81 | for node in json_data['nodes']: 82 | if not all(key in node for key in ['id', 'label']): 83 | return None 84 | node['shape'] = node.get('shape', 'box') 85 | node['level'] = node.get('level', 0) 86 | node['order'] = node.get('order', 1) 87 | 88 | for edge in json_data['edges']: 89 | if not all(key in edge for key in ['from', 'to']): 90 | return None 91 | edge['order'] = edge.get('order', 1) 92 | 93 | return json_data 94 | except json.JSONDecodeError: 95 | return None 96 | 97 | def generate_flowchart(topic, chart_type, animation, detail_level, document_text=None): 98 | max_text_length = 4000 99 | if document_text: 100 | topic_prompt = f"Generate a hierarchical {'mind map' if chart_type == 'mind_map' else 'flowchart'} based on this content:\n\n{document_text}\n\n" 101 | else: 102 | topic_prompt = f"Generate a hierarchical {'mind map' if chart_type == 'mind_map' else 'flowchart'} for: \"{topic}\".\n\n" 103 | 104 | prompt = topic_prompt + f""" 105 | Please create a {'mind map' if chart_type == 'mind_map' else 'flowchart'} that is {'animated' if animation == 'animated' else 'static'} and {'simple' if detail_level == 'simple' else 'normal' if detail_level == 'normal' else 'detailed'}. 106 | 107 | Output a JSON object with this exact structure: 108 | {{ 109 | "nodes": [ 110 | {{"id": 1, "label": "Start", "shape": "ellipse", "level": 0, "order": 1}}, 111 | {{"id": 2, "label": "Process", "shape": "box", "level": 1, "order": 2}} 112 | ], 113 | "edges": [ 114 | {{"from": 1, "to": 2, "order": 1}} 115 | ] 116 | }} 117 | 118 | Rules: 119 | 1. Use only these shapes: "ellipse", "box", "diamond", "hexagon", "circle" 120 | 2. Each node must have a unique integer id 121 | 3. Level 0 is root, increasing for each sub-level 122 | 4. Include order for animation sequence 123 | 5. Keep labels clear and concise 124 | 6. Maximum 20 nodes for simple, 35 for normal, 50 for detailed 125 | 7. Output ONLY the JSON, no other text""" 126 | 127 | try: 128 | response = model.generate_content(prompt) 129 | flowchart_data = clean_and_validate_json(response.text) 130 | 131 | if flowchart_data is None: 132 | return {"error": "Invalid JSON structure", "raw_response": response.text} 133 | 134 | return flowchart_data 135 | except Exception as e: 136 | return {"error": f"Error generating flowchart: {str(e)}"} 137 | 138 | def modify_flowchart(current_data, prompt, chart_type): 139 | """Modifies the current flowchart based on a user prompt.""" 140 | current_data_str = json.dumps(current_data) 141 | prompt_text = f"""Given the current {'mind map' if chart_type == 'mind_map' else 'flowchart'} data:\n\n{current_data_str}\n\nModify it according to the following prompt: \"{prompt}\". 142 | 143 | The output should be a JSON object with the same structure as before, representing the updated {'mind map' if chart_type == 'mind_map' else 'flowchart'}. Ensure that the node and edge IDs remain unique and consistent where applicable. 144 | 145 | Output ONLY the JSON, no other text.""" 146 | 147 | try: 148 | response = model.generate_content(prompt_text) 149 | modified_data = clean_and_validate_json(response.text) 150 | 151 | if modified_data is None: 152 | return {"error": "Invalid JSON structure from modification", "raw_response": response.text} 153 | 154 | return modified_data 155 | except Exception as e: 156 | return {"error": f"Error modifying flowchart: {str(e)}"} 157 | 158 | @app.route('/') 159 | def index(): 160 | return render_template('index.html') 161 | 162 | @app.route('/get_flowchart_data', methods=['POST']) 163 | def get_flowchart_data(): 164 | global current_flowchart_data 165 | try: 166 | data = request.form 167 | topic = data.get('topic', '').strip() 168 | chart_type = data.get('type', 'flowchart') 169 | animation = data.get('animation', 'static') 170 | detail_level = data.get('detail_level', 'normal') 171 | 172 | document_text = None 173 | file = request.files.get('file') 174 | 175 | if file and file.filename: 176 | if not allowed_file(file.filename): 177 | return jsonify({"error": "Unsupported file type."}), 400 178 | 179 | filename = secure_filename(file.filename) 180 | temp_fd, temp_path = tempfile.mkstemp() 181 | 182 | try: 183 | with os.fdopen(temp_fd, 'wb') as temp_file: 184 | file.save(temp_file) 185 | 186 | if filename.lower().endswith('.docx'): 187 | document_text = extract_text_from_docx(temp_path) 188 | elif filename.lower().endswith('.pdf'): 189 | document_text = extract_text_from_pdf(temp_path) 190 | 191 | if not topic and document_text: 192 | topic = "Flowchart from Document" 193 | 194 | finally: 195 | if os.path.exists(temp_path): 196 | os.remove(temp_path) 197 | 198 | if not topic and not file: 199 | return jsonify({"error": "Please provide a topic or upload a document."}), 400 200 | 201 | flowchart_data = generate_flowchart(topic, chart_type, animation, detail_level, document_text) 202 | 203 | if 'error' in flowchart_data: 204 | return jsonify(flowchart_data), 500 205 | 206 | current_flowchart_data = flowchart_data # Store the generated data 207 | 208 | nodes = [{ 209 | "id": node["id"], 210 | "label": node["label"], 211 | "shape": node.get("shape", "box"), 212 | "order": node.get("order", 1), 213 | "level": node.get("level", 0) 214 | } for node in flowchart_data.get('nodes', [])] 215 | 216 | edges = [{ 217 | "from": edge["from"], 218 | "to": edge["to"], 219 | "id": f"{edge['from']}-{edge['to']}", 220 | "order": edge.get("order", 1) 221 | } for edge in flowchart_data.get('edges', [])] 222 | 223 | nodes.sort(key=lambda x: x['order']) 224 | edges.sort(key=lambda x: x['order']) 225 | 226 | return jsonify({ 227 | "nodes": nodes, 228 | "edges": edges, 229 | "animation": animation, 230 | "chart_type": chart_type 231 | }) 232 | 233 | except Exception as e: 234 | logging.error(f"Error in get_flowchart_data: {str(e)}") 235 | return jsonify({"error": "An unexpected error occurred."}), 500 236 | 237 | @app.route('/add_node', methods=['POST']) 238 | def add_node(): 239 | global current_flowchart_data 240 | data = request.get_json() 241 | new_node = data.get('node') 242 | if new_node: 243 | # Simple way to generate a new unique ID (can be improved) 244 | new_id = max([node['id'] for node in current_flowchart_data['nodes']] or [0]) + 1 245 | new_node['id'] = new_id 246 | current_flowchart_data['nodes'].append(new_node) 247 | return jsonify({"status": "success", "node": new_node}) 248 | return jsonify({"status": "error", "message": "Invalid node data"}), 400 249 | 250 | @app.route('/delete_node/', methods=['DELETE']) 251 | def delete_node(node_id): 252 | global current_flowchart_data 253 | current_flowchart_data['nodes'] = [node for node in current_flowchart_data['nodes'] if node['id'] != node_id] 254 | current_flowchart_data['edges'] = [edge for edge in current_flowchart_data['edges'] 255 | if edge['from'] != node_id and edge['to'] != node_id] 256 | return jsonify({"status": "success"}) 257 | 258 | @app.route('/edit_node/', methods=['PUT']) 259 | def edit_node(node_id): 260 | global current_flowchart_data 261 | data = request.get_json() 262 | new_label = data.get('node').get('label') 263 | for node in current_flowchart_data['nodes']: 264 | if node['id'] == node_id: 265 | node['label'] = new_label 266 | return jsonify({"status": "success", "node": node}) 267 | return jsonify({"status": "error", "message": "Node not found"}), 404 268 | 269 | @app.route('/add_edge', methods=['POST']) 270 | def add_edge(): 271 | global current_flowchart_data 272 | data = request.get_json() 273 | new_edge = data.get('edge') 274 | if new_edge: 275 | current_flowchart_data['edges'].append(new_edge) 276 | return jsonify({"status": "success", "edge": new_edge}) 277 | return jsonify({"status": "error", "message": "Invalid edge data"}), 400 278 | 279 | @app.route('/delete_edge//', methods=['DELETE']) 280 | def delete_edge(from_id, to_id): 281 | global current_flowchart_data 282 | current_flowchart_data['edges'] = [ 283 | edge for edge in current_flowchart_data['edges'] 284 | if not (str(edge['from']) == from_id and str(edge['to']) == to_id) 285 | ] 286 | return jsonify({"status": "success"}) 287 | 288 | @app.route('/modify_flowchart_prompt', methods=['POST']) 289 | def modify_flowchart_prompt(): 290 | global current_flowchart_data, is_chart_modifying 291 | data = request.get_json() 292 | prompt = data.get('prompt') 293 | chart_type = data.get('chart_type', 'flowchart') 294 | 295 | if not prompt: 296 | return jsonify({"status": "error", "message": "Prompt cannot be empty"}), 400 297 | 298 | if is_chart_modifying: 299 | return jsonify({"status": "error", "message": "Chart is currently being modified, please wait..."}), 400 300 | 301 | is_chart_modifying = True # set flag 302 | try: 303 | modified_data = modify_flowchart(current_flowchart_data, prompt, chart_type) 304 | 305 | if 'error' in modified_data: 306 | return jsonify(modified_data), 500 307 | 308 | current_flowchart_data = modified_data # Update the current data 309 | 310 | # Prepare the data for vis-network 311 | nodes = [{ 312 | "id": node["id"], 313 | "label": node["label"], 314 | "shape": node.get("shape", "box"), 315 | "order": node.get("order", 1), 316 | "level": node.get("level", 0) 317 | } for node in modified_data.get('nodes', [])] 318 | 319 | edges = [{ 320 | "from": edge["from"], 321 | "to": edge["to"], 322 | "id": f"{edge['from']}-{edge['to']}", 323 | "order": edge.get("order", 1) 324 | } for edge in modified_data.get('edges', [])] 325 | 326 | return jsonify({ 327 | "status": "success", 328 | "nodes": nodes, 329 | "edges": edges 330 | }) 331 | except Exception as e: 332 | return jsonify({"error": f"Error modifying flowchart: {str(e)}"}) 333 | finally: 334 | is_chart_modifying = False # clear flag 335 | 336 | if __name__ == '__main__': 337 | app.run(debug=True) 338 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | ChartForge - Visualize Ideas Beautifully 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 27 | 791 | 792 | 793 |
794 |
795 |
796 | 799 |

ChartForge

800 |

801 | Visualize your ideas and knowledge with interactive charts. 802 |

803 |
804 |
805 |
806 |
807 |
808 | 809 | 815 |
816 |
817 | 818 | 824 |
825 |
826 | 827 | 831 |
832 |
833 | 834 | 838 |
839 |
840 | 841 | 846 |
847 |
848 | 851 |
852 |
853 |
854 |
855 |
856 |
857 |
Crafting Your Visual...
858 |
859 | 862 | 865 | 868 | 871 |
872 | 873 | 876 | 879 | 882 | 885 |
886 |
887 |
888 | 889 | 893 | 896 |
897 |
898 |
899 | 900 | 916 | 917 | 1551 | 1552 | 1553 | --------------------------------------------------------------------------------