Spaces:
Running
Running
| """ | |
| Knowledge Graph Routes | |
| Node-based revision notes system using React Flow | |
| """ | |
| from flask import Blueprint, render_template, request, jsonify, current_app, url_for | |
| from flask_login import login_required, current_user | |
| from utils import get_db_connection | |
| import os | |
| import json | |
| import base64 | |
| from datetime import datetime | |
| from werkzeug.utils import secure_filename | |
| knowledge_bp = Blueprint('knowledge_bp', __name__) | |
| # Allowed PDF extensions | |
| ALLOWED_EXTENSIONS = {'pdf'} | |
| def allowed_file(filename): | |
| return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS | |
| def knowledge_graph_index(): | |
| """Main knowledge graph page.""" | |
| return render_template('knowledge_graph.html') | |
| def upload_pdf(): | |
| """ | |
| Upload PDF and create session with page images. | |
| Returns session_id and page information for creating nodes. | |
| """ | |
| try: | |
| if 'pdf' not in request.files: | |
| return jsonify({'error': 'No PDF file provided'}), 400 | |
| file = request.files['pdf'] | |
| if file.filename == '': | |
| return jsonify({'error': 'No file selected'}), 400 | |
| if not allowed_file(file.filename): | |
| return jsonify({'error': 'Only PDF files are allowed'}), 400 | |
| # Create session for this knowledge graph | |
| conn = get_db_connection() | |
| session_id = f"kg_{current_user.id}_{int(datetime.now().timestamp())}" | |
| # Save session metadata | |
| conn.execute(""" | |
| INSERT INTO sessions (id, user_id, original_filename, session_type, created_at) | |
| VALUES (?, ?, ?, 'knowledge_graph', ?) | |
| """, (session_id, current_user.id, secure_filename(file.filename), datetime.now())) | |
| # Save PDF file | |
| pdf_filename = f"{session_id}_{secure_filename(file.filename)}" | |
| pdf_path = os.path.join(current_app.config['UPLOAD_FOLDER'], pdf_filename) | |
| file.save(pdf_path) | |
| conn.commit() | |
| # TODO: Process PDF to extract pages | |
| # For now, we'll create placeholder page nodes | |
| # In production, use PyMuPDF or pdf2image to convert PDF pages to images | |
| pages = [] | |
| # Placeholder: Create 5 dummy pages | |
| # Replace this with actual PDF processing | |
| for i in range(1, 6): | |
| pages.append({ | |
| 'page_number': i, | |
| 'image_url': f'/placeholder_page/{session_id}/{i}' # Placeholder route | |
| }) | |
| conn.close() | |
| return jsonify({ | |
| 'success': True, | |
| 'session_id': session_id, | |
| 'pdf_filename': pdf_filename, | |
| 'pages': pages | |
| }) | |
| except Exception as e: | |
| current_app.logger.error(f"Error uploading PDF: {e}") | |
| return jsonify({'error': str(e)}), 500 | |
| def save_graph(): | |
| """ | |
| Save the node graph (nodes and edges) to database. | |
| """ | |
| try: | |
| data = request.json | |
| session_id = data.get('session_id') | |
| nodes = data.get('nodes', []) | |
| edges = data.get('edges', []) | |
| if not session_id: | |
| return jsonify({'error': 'Session ID required'}), 400 | |
| # Validate ownership | |
| conn = get_db_connection() | |
| session = conn.execute( | |
| "SELECT user_id FROM sessions WHERE id = ? AND session_type = 'knowledge_graph'", | |
| (session_id,) | |
| ).fetchone() | |
| if not session or session['user_id'] != current_user.id: | |
| conn.close() | |
| return jsonify({'error': 'Unauthorized'}), 403 | |
| # Save graph data as JSON | |
| graph_data = { | |
| 'nodes': nodes, | |
| 'edges': edges, | |
| 'saved_at': datetime.now().isoformat() | |
| } | |
| conn.execute(""" | |
| INSERT OR REPLACE INTO knowledge_graphs (session_id, graph_data, updated_at) | |
| VALUES (?, ?, ?) | |
| """, (session_id, json.dumps(graph_data), datetime.now())) | |
| conn.commit() | |
| conn.close() | |
| return jsonify({ | |
| 'success': True, | |
| 'message': 'Graph saved successfully' | |
| }) | |
| except Exception as e: | |
| current_app.logger.error(f"Error saving graph: {e}") | |
| return jsonify({'error': str(e)}), 500 | |
| def load_graph(session_id): | |
| """ | |
| Load a saved node graph. | |
| """ | |
| try: | |
| conn = get_db_connection() | |
| # Validate ownership | |
| session = conn.execute( | |
| "SELECT user_id FROM sessions WHERE id = ? AND session_type = 'knowledge_graph'", | |
| (session_id,) | |
| ).fetchone() | |
| if not session or session['user_id'] != current_user.id: | |
| conn.close() | |
| return jsonify({'error': 'Unauthorized'}), 403 | |
| # Load graph data | |
| graph = conn.execute( | |
| "SELECT graph_data FROM knowledge_graphs WHERE session_id = ?", | |
| (session_id,) | |
| ).fetchone() | |
| conn.close() | |
| if not graph or not graph['graph_data']: | |
| return jsonify({ | |
| 'success': True, | |
| 'nodes': [], | |
| 'edges': [] | |
| }) | |
| graph_data = json.loads(graph['graph_data']) | |
| return jsonify({ | |
| 'success': True, | |
| 'nodes': graph_data.get('nodes', []), | |
| 'edges': graph_data.get('edges', []), | |
| 'saved_at': graph_data.get('saved_at') | |
| }) | |
| except Exception as e: | |
| current_app.logger.error(f"Error loading graph: {e}") | |
| return jsonify({'error': str(e)}), 500 | |
| def list_graphs(): | |
| """ | |
| List all knowledge graphs for the current user. | |
| """ | |
| try: | |
| conn = get_db_connection() | |
| graphs = conn.execute(""" | |
| SELECT s.id, s.original_filename, s.created_at, kg.updated_at | |
| FROM sessions s | |
| LEFT JOIN knowledge_graphs kg ON s.id = kg.session_id | |
| WHERE s.user_id = ? AND s.session_type = 'knowledge_graph' | |
| ORDER BY s.created_at DESC | |
| """, (current_user.id,)).fetchall() | |
| conn.close() | |
| return jsonify({ | |
| 'success': True, | |
| 'graphs': [dict(g) for g in graphs] | |
| }) | |
| except Exception as e: | |
| current_app.logger.error(f"Error listing graphs: {e}") | |
| return jsonify({'error': str(e)}), 500 | |
| def delete_graph(session_id): | |
| """ | |
| Delete a knowledge graph. | |
| """ | |
| try: | |
| conn = get_db_connection() | |
| # Validate ownership | |
| session = conn.execute( | |
| "SELECT user_id FROM sessions WHERE id = ? AND session_type = 'knowledge_graph'", | |
| (session_id,) | |
| ).fetchone() | |
| if not session or session['user_id'] != current_user.id: | |
| conn.close() | |
| return jsonify({'error': 'Unauthorized'}), 403 | |
| # Delete graph data and session | |
| conn.execute("DELETE FROM knowledge_graphs WHERE session_id = ?", (session_id,)) | |
| conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,)) | |
| conn.commit() | |
| conn.close() | |
| return jsonify({ | |
| 'success': True, | |
| 'message': 'Graph deleted successfully' | |
| }) | |
| except Exception as e: | |
| current_app.logger.error(f"Error deleting graph: {e}") | |
| return jsonify({'error': str(e)}), 500 | |
| def placeholder_page(session_id, page_num): | |
| """ | |
| Placeholder route for page images. | |
| Returns a simple placeholder image. | |
| """ | |
| from flask import send_file | |
| from PIL import Image, ImageDraw, ImageFont | |
| import io | |
| # Create a placeholder image | |
| img = Image.new('RGB', (400, 500), color='#2b3035') | |
| draw = ImageDraw.Draw(img) | |
| # Draw text | |
| text = f"Page {page_num}" | |
| draw.text((200, 250), text, fill='#ffffff', anchor='mm') | |
| # Save to buffer | |
| buffer = io.BytesIO() | |
| img.save(buffer, format='PNG') | |
| buffer.seek(0) | |
| return send_file(buffer, mimetype='image/png') | |
| def add_question(): | |
| """ | |
| Add a question node from existing session data. | |
| """ | |
| try: | |
| data = request.json | |
| session_id = data.get('session_id') | |
| question_data = data.get('question', {}) | |
| # Validate and fetch question from database | |
| conn = get_db_connection() | |
| question = conn.execute(""" | |
| SELECT q.*, i.filename as image_filename | |
| FROM questions q | |
| LEFT JOIN images i ON q.image_id = i.id | |
| WHERE q.session_id = ? AND q.id = ? | |
| """, (session_id, question_data.get('id'))).fetchone() | |
| if not question: | |
| conn.close() | |
| return jsonify({'error': 'Question not found'}), 404 | |
| conn.close() | |
| # Return question data formatted for node creation | |
| return jsonify({ | |
| 'success': True, | |
| 'node': { | |
| 'id': f"question-{question['id']}", | |
| 'type': 'question', | |
| 'data': { | |
| 'number': question.get('question_number', '?'), | |
| 'subject': question.get('subject', ''), | |
| 'chapter': question.get('chapter', ''), | |
| 'question_id': question['id'] | |
| } | |
| } | |
| }) | |
| except Exception as e: | |
| current_app.logger.error(f"Error adding question: {e}") | |
| return jsonify({'error': str(e)}), 500 | |
| def add_note(): | |
| """ | |
| Add a note node from existing revision notes. | |
| """ | |
| try: | |
| data = request.json | |
| image_id = data.get('image_id') | |
| conn = get_db_connection() | |
| # Fetch note from database | |
| image = conn.execute(""" | |
| SELECT note_json, note_filename | |
| FROM images | |
| WHERE id = ? AND note_json IS NOT NULL | |
| """, (image_id,)).fetchone() | |
| if not image: | |
| conn.close() | |
| return jsonify({'error': 'Note not found'}), 404 | |
| conn.close() | |
| note_content = image['note_json'] or '{}' | |
| return jsonify({ | |
| 'success': True, | |
| 'node': { | |
| 'id': f"note-{image_id}", | |
| 'type': 'note', | |
| 'data': { | |
| 'content': note_content, | |
| 'image_id': image_id, | |
| 'note_image': image['note_filename'] | |
| } | |
| } | |
| }) | |
| except Exception as e: | |
| current_app.logger.error(f"Error adding note: {e}") | |
| return jsonify({'error': str(e)}), 500 | |