Spaces:
Running
Running
File size: 11,386 Bytes
e8a57cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | """
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
@knowledge_bp.route('/knowledge_graph')
@login_required
def knowledge_graph_index():
"""Main knowledge graph page."""
return render_template('knowledge_graph.html')
@knowledge_bp.route('/knowledge_graph/upload_pdf', methods=['POST'])
@login_required
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
@knowledge_bp.route('/knowledge_graph/save', methods=['POST'])
@login_required
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
@knowledge_bp.route('/knowledge_graph/load/<session_id>')
@login_required
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
@knowledge_bp.route('/knowledge_graph/list')
@login_required
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
@knowledge_bp.route('/knowledge_graph/delete/<session_id>', methods=['POST'])
@login_required
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
@knowledge_bp.route('/placeholder_page/<session_id>/<int:page_num>')
@login_required
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')
@knowledge_bp.route('/knowledge_graph/add_question', methods=['POST'])
@login_required
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
@knowledge_bp.route('/knowledge_graph/add_note', methods=['POST'])
@login_required
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
|