signalpilot-ai-internal 0.5.1__py3-none-any.whl → 0.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. signalpilot_ai_internal/_version.py +1 -1
  2. signalpilot_ai_internal/cache_service.py +152 -1
  3. signalpilot_ai_internal/file_scanner_service.py +1395 -0
  4. signalpilot_ai_internal/handlers.py +478 -2
  5. signalpilot_ai_internal/html_export_template/README.md +23 -0
  6. signalpilot_ai_internal/html_export_template/conf.json +12 -0
  7. signalpilot_ai_internal/html_export_template/index.html.j2 +140 -0
  8. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json +2 -2
  9. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig +1 -1
  10. signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/490.b4ccb9601c8112407c5d.js +1 -0
  11. signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/839.ed04fa601a43e8dd24d1.js +1 -0
  12. signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.4e9edb7f224152c1dcb4.js +2 -0
  13. signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.ee8951353b00c13b8070.js +1 -0
  14. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/third-party-licenses.json +0 -6
  15. {signalpilot_ai_internal-0.5.1.dist-info → signalpilot_ai_internal-0.7.6.dist-info}/METADATA +3 -1
  16. signalpilot_ai_internal-0.7.6.dist-info/RECORD +49 -0
  17. signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/104.04e170724f369fcbaf19.js +0 -2
  18. signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/104.04e170724f369fcbaf19.js.LICENSE.txt +0 -24
  19. signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/188.e781cc4c87f2dbf290ec.js +0 -1
  20. signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/606.90aaaae46b73dc3c08fb.js +0 -1
  21. signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/839.7ea0c8f6af45369912f3.js +0 -1
  22. signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.5251a593584dd5d131d5.js +0 -2
  23. signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.59ffb91489066223094b.js +0 -1
  24. signalpilot_ai_internal-0.5.1.dist-info/RECORD +0 -48
  25. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/etc/jupyter/jupyter_server_config.d/signalpilot_ai.json +0 -0
  26. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/install.json +0 -0
  27. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/plugin.json +0 -0
  28. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -0
  29. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -0
  30. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -0
  31. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.72484b768a04f89bd3dd.js +0 -0
  32. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -0
  33. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -0
  34. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -0
  35. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.9b4f05a99f5003f82094.js +0 -0
  36. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -0
  37. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -0
  38. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -0
  39. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js.LICENSE.txt +0 -0
  40. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -0
  41. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -0
  42. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -0
  43. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -0
  44. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.3aa564fc148b37d1d719.js +0 -0
  45. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -0
  46. /signalpilot_ai_internal-0.5.1.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.5251a593584dd5d131d5.js.LICENSE.txt → /signalpilot_ai_internal-0.7.6.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/898.4e9edb7f224152c1dcb4.js.LICENSE.txt +0 -0
  47. {signalpilot_ai_internal-0.5.1.data → signalpilot_ai_internal-0.7.6.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/style.js +0 -0
  48. {signalpilot_ai_internal-0.5.1.dist-info → signalpilot_ai_internal-0.7.6.dist-info}/WHEEL +0 -0
  49. {signalpilot_ai_internal-0.5.1.dist-info → signalpilot_ai_internal-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -12,6 +12,7 @@ from .cache_service import get_cache_service
12
12
  from .cache_handlers import ChatHistoriesHandler, AppValuesHandler, CacheInfoHandler
13
13
  from .unified_database_schema_service import UnifiedDatabaseSchemaHandler, UnifiedDatabaseQueryHandler
14
14
  from .snowflake_schema_service import SnowflakeSchemaHandler, SnowflakeQueryHandler
15
+ from .file_scanner_service import get_file_scanner_service
15
16
  from .schema_search_service import SchemaSearchHandler
16
17
 
17
18
 
@@ -258,6 +259,447 @@ class ReadAllFilesHandler(APIHandler):
258
259
  return '\n'.join(lines)
259
260
 
260
261
 
262
+ class SelectFolderHandler(APIHandler):
263
+ """Handler to open a native folder picker and return the selected absolute path"""
264
+
265
+ # Class-level flag to prevent multiple dialogs
266
+ _dialog_open = False
267
+
268
+ @tornado.web.authenticated
269
+ def get(self):
270
+ # Check if a dialog is already open
271
+ if SelectFolderHandler._dialog_open:
272
+ self.set_status(409) # Conflict status
273
+ self.finish(json.dumps({
274
+ "error": "A folder selection dialog is already open"
275
+ }))
276
+ return
277
+
278
+ try:
279
+ import tkinter as tk
280
+ from tkinter import filedialog
281
+ import threading
282
+ import time
283
+
284
+ # Set flag to prevent multiple dialogs
285
+ SelectFolderHandler._dialog_open = True
286
+
287
+ # Create a fresh tkinter instance
288
+ root = tk.Tk()
289
+
290
+ # Position the root window in the center of the screen BEFORE withdrawing
291
+ try:
292
+ # Get screen dimensions
293
+ screen_width = root.winfo_screenwidth()
294
+ screen_height = root.winfo_screenheight()
295
+
296
+ # Calculate center position
297
+ x = (screen_width // 2) - 200 # Dialog is roughly 400px wide
298
+ y = (screen_height // 2) - 150 # Dialog is roughly 300px tall
299
+
300
+ # Set window position and make it visible briefly for positioning
301
+ root.geometry(f"400x300+{x}+{y}")
302
+ root.update_idletasks()
303
+
304
+ # Now withdraw the window
305
+ root.withdraw()
306
+
307
+ # Enhanced topmost settings for better visibility
308
+ root.attributes('-topmost', True)
309
+ root.lift()
310
+ root.focus_force()
311
+
312
+ # Final positioning update
313
+ root.update_idletasks()
314
+ except Exception:
315
+ # Fallback: just withdraw if positioning fails
316
+ root.withdraw()
317
+
318
+ folder = None
319
+
320
+ try:
321
+ # Show the dialog with proper positioning
322
+ folder = filedialog.askdirectory(
323
+ parent=root,
324
+ title="Select Folder",
325
+ initialdir=os.path.expanduser("~") # Start in user's home directory
326
+ )
327
+ except Exception as e:
328
+ raise e
329
+ finally:
330
+ # Comprehensive cleanup
331
+ try:
332
+ # Force close and destroy all tkinter components
333
+ root.quit()
334
+ root.destroy()
335
+
336
+ # Additional cleanup for macOS - ensure complete destruction
337
+ try:
338
+ root.update_idletasks()
339
+ root.update()
340
+ # Force garbage collection of tkinter objects
341
+ import gc
342
+ gc.collect()
343
+ except Exception:
344
+ pass
345
+
346
+ except Exception:
347
+ pass
348
+ finally:
349
+ # Reset flag and add small delay to ensure cleanup
350
+ SelectFolderHandler._dialog_open = False
351
+ time.sleep(0.1) # Small delay to ensure cleanup completes
352
+
353
+ # Normalize and return absolute path or null
354
+ if folder:
355
+ folder_path = os.path.abspath(folder)
356
+ self.finish(json.dumps({"path": folder_path}))
357
+ else:
358
+ self.finish(json.dumps({"path": None}))
359
+
360
+ except Exception as e:
361
+ # Reset flag on error
362
+ SelectFolderHandler._dialog_open = False
363
+ self.set_status(400)
364
+ self.finish(json.dumps({
365
+ "error": str(e)
366
+ }))
367
+
368
+ class FileScanHandler(APIHandler):
369
+ """Handler for scanning directories for files"""
370
+
371
+ @tornado.web.authenticated
372
+ async def post(self):
373
+ try:
374
+ data = json.loads(self.request.body.decode('utf-8'))
375
+ paths = data.get('paths', [])
376
+
377
+ if not paths:
378
+ self.set_status(400)
379
+ self.finish(json.dumps({
380
+ "error": "No paths provided"
381
+ }))
382
+ return
383
+
384
+ file_scanner = get_file_scanner_service()
385
+ # Pass the current working directory as the workspace root for relative path calculation
386
+ result = await file_scanner.scan_directories(paths, workspace_root=os.getcwd())
387
+
388
+ # Update scanned directories tracking
389
+ scanned_dirs = file_scanner.get_scanned_directories()
390
+ current_dirs = scanned_dirs.get('directories', [])
391
+
392
+ # Update directory metadata
393
+ for new_dir in result['scanned_directories']:
394
+ # Check if directory already exists
395
+ existing_dir = None
396
+ for existing in current_dirs:
397
+ if existing['path'] == new_dir['path']:
398
+ existing_dir = existing
399
+ break
400
+
401
+ if existing_dir:
402
+ # Update existing directory
403
+ existing_dir['file_count'] = new_dir['file_count']
404
+ existing_dir['scanned_at'] = new_dir['scanned_at']
405
+ else:
406
+ # Add new directory
407
+ current_dirs.append(new_dir)
408
+
409
+ # Save updated directories list
410
+ file_scanner.update_scanned_directories(current_dirs)
411
+
412
+ self.finish(json.dumps(result))
413
+
414
+ except Exception as e:
415
+ self.set_status(500)
416
+ self.finish(json.dumps({
417
+ "error": str(e)
418
+ }))
419
+
420
+
421
+ class ScannedDirectoriesHandler(APIHandler):
422
+ """Handler for getting scanned directories list"""
423
+
424
+ @tornado.web.authenticated
425
+ def get(self):
426
+ try:
427
+ file_scanner = get_file_scanner_service()
428
+ result = file_scanner.get_scanned_directories()
429
+
430
+ self.finish(json.dumps(result))
431
+
432
+ except Exception as e:
433
+ self.set_status(500)
434
+ self.finish(json.dumps({
435
+ "error": str(e)
436
+ }))
437
+
438
+
439
+ class WorkDirHandler(APIHandler):
440
+ """Handler for returning current working directory"""
441
+
442
+ @tornado.web.authenticated
443
+ def get(self):
444
+ try:
445
+ self.finish(json.dumps({"workdir": os.getcwd()}))
446
+ except Exception as e:
447
+ self.set_status(500)
448
+ self.finish(json.dumps({
449
+ "error": str(e)
450
+ }))
451
+
452
+
453
+ class TerminalExecuteHandler(APIHandler):
454
+ """Handler for executing terminal commands"""
455
+
456
+ @tornado.web.authenticated
457
+ def post(self):
458
+ try:
459
+ data = json.loads(self.request.body.decode('utf-8'))
460
+ command = data.get('command')
461
+
462
+ if not command:
463
+ self.set_status(400)
464
+ self.finish(json.dumps({
465
+ "error": "No command provided"
466
+ }))
467
+ return
468
+
469
+ import subprocess
470
+ result = subprocess.run(
471
+ command,
472
+ shell=True,
473
+ cwd=os.getcwd(),
474
+ capture_output=True,
475
+ text=True,
476
+ timeout=300
477
+ )
478
+
479
+ def truncate_output(output: str, max_lines: int = 50) -> str:
480
+ if not output:
481
+ return output
482
+ lines = output.splitlines()
483
+ if len(lines) <= max_lines * 2:
484
+ return output
485
+ first_lines = lines[:max_lines]
486
+ last_lines = lines[-max_lines:]
487
+ truncated_count = len(lines) - (max_lines * 2)
488
+ return '\n'.join(first_lines + [f'\n... {truncated_count} lines truncated ...\n'] + last_lines)
489
+
490
+ self.finish(json.dumps({
491
+ "stdout": truncate_output(result.stdout),
492
+ "stderr": truncate_output(result.stderr),
493
+ "exit_code": result.returncode
494
+ }))
495
+
496
+ except subprocess.TimeoutExpired:
497
+ self.set_status(408)
498
+ self.finish(json.dumps({
499
+ "error": "Command timed out after 300 seconds"
500
+ }))
501
+ except Exception as e:
502
+ self.set_status(500)
503
+ self.finish(json.dumps({
504
+ "error": str(e)
505
+ }))
506
+
507
+
508
+ class DeleteScannedDirectoryHandler(APIHandler):
509
+ """Handler for deleting a scanned directory"""
510
+
511
+ @tornado.web.authenticated
512
+ def post(self):
513
+ try:
514
+ data = json.loads(self.request.body.decode('utf-8'))
515
+ directory_path = data.get('path')
516
+
517
+ if not directory_path:
518
+ self.set_status(400)
519
+ self.finish(json.dumps({
520
+ "error": "No directory path provided"
521
+ }))
522
+ return
523
+
524
+ file_scanner = get_file_scanner_service()
525
+
526
+ # Get current scanned directories
527
+ current_directories = file_scanner.get_scanned_directories()
528
+ directories = current_directories.get('directories', [])
529
+
530
+ # Filter out the directory to be deleted
531
+ filtered_directories = [
532
+ dir_info for dir_info in directories
533
+ if dir_info.get('path') != directory_path
534
+ ]
535
+
536
+ # Check if directory was actually found and removed
537
+ if len(filtered_directories) == len(directories):
538
+ self.set_status(404)
539
+ self.finish(json.dumps({
540
+ "error": f"Directory '{directory_path}' not found in scanned directories"
541
+ }))
542
+ return
543
+
544
+ # Update the scanned directories list
545
+ success = file_scanner.update_scanned_directories(filtered_directories)
546
+
547
+ if success:
548
+ self.finish(json.dumps({
549
+ "success": True,
550
+ "message": f"Directory '{directory_path}' removed from scanning"
551
+ }))
552
+ else:
553
+ self.set_status(500)
554
+ self.finish(json.dumps({
555
+ "error": "Failed to update scanned directories"
556
+ }))
557
+
558
+ except Exception as e:
559
+ self.set_status(500)
560
+ self.finish(json.dumps({
561
+ "error": str(e)
562
+ }))
563
+
564
+
565
+ class NotebookCellsHandler(APIHandler):
566
+ """Handler for reading specific cell ranges from notebooks"""
567
+
568
+ @tornado.web.authenticated
569
+ def post(self):
570
+ try:
571
+ data = json.loads(self.request.body.decode('utf-8'))
572
+ file_path = data.get('file_path')
573
+ start_cell = data.get('start_cell', 0)
574
+ end_cell = data.get('end_cell', 5)
575
+
576
+ if not file_path:
577
+ self.set_status(400)
578
+ self.finish(json.dumps({
579
+ "error": "No file_path provided"
580
+ }))
581
+ return
582
+
583
+ file_scanner = get_file_scanner_service()
584
+ result = file_scanner.extract_schema(file_path, start_cell=start_cell, end_cell=end_cell)
585
+
586
+ self.finish(json.dumps(result))
587
+
588
+ except Exception as e:
589
+ self.set_status(500)
590
+ self.finish(json.dumps({
591
+ "error": str(e)
592
+ }))
593
+
594
+
595
+ class NotebookToHTMLHandler(APIHandler):
596
+ """Handler for converting notebooks to HTML using nbconvert"""
597
+
598
+ @tornado.web.authenticated
599
+ def post(self):
600
+ try:
601
+ # Parse request body
602
+ try:
603
+ data = json.loads(self.request.body.decode('utf-8'))
604
+ except json.JSONDecodeError:
605
+ self.set_status(400)
606
+ self.finish(json.dumps({
607
+ "error": "Invalid JSON in request body"
608
+ }))
609
+ return
610
+
611
+ # Get notebook path
612
+ notebook_path = data.get('notebook_path')
613
+ if not notebook_path:
614
+ self.set_status(400)
615
+ self.finish(json.dumps({
616
+ "error": "No notebook path provided"
617
+ }))
618
+ return
619
+
620
+ # Get export options with defaults
621
+ include_input = data.get('include_input', True)
622
+ include_output = data.get('include_output', True)
623
+ include_images = data.get('include_images', True)
624
+
625
+ # Convert to nbconvert options
626
+ exclude_input = not include_input
627
+ exclude_output = not include_output
628
+
629
+ try:
630
+ # Import nbconvert and nbformat
631
+ from nbconvert import HTMLExporter
632
+ from nbconvert.preprocessors import ExtractOutputPreprocessor
633
+ from nbformat import read
634
+
635
+ # Read the notebook file using nbformat
636
+ notebook_file_path = Path(notebook_path)
637
+ if not notebook_file_path.exists():
638
+ self.set_status(404)
639
+ self.finish(json.dumps({
640
+ "error": f"Notebook file not found: {notebook_path}"
641
+ }))
642
+ return
643
+
644
+ # Load notebook using nbformat.read (more robust)
645
+ with open(notebook_file_path, 'r', encoding='utf-8') as f:
646
+ notebook_content = read(f, as_version=4)
647
+
648
+ # Get the custom template path (relative to this file)
649
+ template_dir = Path(__file__).parent / 'html_export_template'
650
+ template_path = str(template_dir.resolve())
651
+
652
+ # Configure the HTML exporter with custom template
653
+ exporter = HTMLExporter(template_name=template_path)
654
+
655
+ # Set export options
656
+ exporter.exclude_input = exclude_input
657
+ exporter.exclude_output = exclude_output
658
+ exporter.exclude_input_prompt = True
659
+ exporter.exclude_output_prompt = True
660
+
661
+ # Configure image handling
662
+ if not include_images:
663
+ # Add preprocessor to exclude images
664
+ exporter.register_preprocessor(ExtractOutputPreprocessor(), enabled=True)
665
+
666
+ # Convert notebook to HTML - nbconvert handles everything
667
+ html_content, _ = exporter.from_notebook_node(notebook_content)
668
+
669
+ # Get absolute path for the notebook
670
+ workspace_notebook_path = str(notebook_file_path.absolute())
671
+
672
+ # Return the HTML content
673
+ self.finish(json.dumps({
674
+ "success": True,
675
+ "html_content": html_content,
676
+ "notebook_path": notebook_path,
677
+ "workspace_notebook_path": workspace_notebook_path,
678
+ "export_options": {
679
+ "include_input": include_input,
680
+ "include_output": include_output,
681
+ "include_images": include_images
682
+ }
683
+ }))
684
+
685
+ except ImportError as e:
686
+ self.set_status(500)
687
+ self.finish(json.dumps({
688
+ "error": f"Required packages not installed: {str(e)}. Please install them with: pip install nbconvert nbformat"
689
+ }))
690
+ except Exception as e:
691
+ self.set_status(500)
692
+ self.finish(json.dumps({
693
+ "error": f"Failed to convert notebook to HTML: {str(e)}"
694
+ }))
695
+
696
+ except Exception as e:
697
+ self.set_status(500)
698
+ self.finish(json.dumps({
699
+ "error": str(e)
700
+ }))
701
+
702
+
261
703
  def setup_handlers(web_app):
262
704
  host_pattern = ".*$"
263
705
  base_url = web_app.settings["base_url"]
@@ -268,6 +710,16 @@ def setup_handlers(web_app):
268
710
  # Read all files endpoint
269
711
  read_all_files_route = url_path_join(base_url, "signalpilot-ai-internal", "read-all-files")
270
712
 
713
+ # File scanning endpoints
714
+ file_scan_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "scan")
715
+ scanned_directories_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "directories")
716
+ select_folder_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "select-folder")
717
+ workdir_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "workdir")
718
+ delete_scanned_directory_route = url_path_join(base_url, "signalpilot-ai-internal", "files", "directories", "delete")
719
+
720
+ # Notebook endpoints
721
+ notebook_cells_route = url_path_join(base_url, "signalpilot-ai-internal", "notebook", "cells")
722
+
271
723
  # Cache service endpoints
272
724
  chat_histories_route = url_path_join(base_url, "signalpilot-ai-internal", "cache", "chat-histories")
273
725
  chat_history_route = url_path_join(base_url, "signalpilot-ai-internal", "cache", "chat-histories", "([^/]+)")
@@ -290,13 +742,32 @@ def setup_handlers(web_app):
290
742
  snowflake_schema_route = url_path_join(base_url, "signalpilot-ai-internal", "snowflake", "schema")
291
743
  snowflake_query_route = url_path_join(base_url, "signalpilot-ai-internal", "snowflake", "query")
292
744
 
745
+ # Notebook HTML export endpoint
746
+ notebook_html_route = url_path_join(base_url, "signalpilot-ai-internal", "notebook", "to-html")
747
+
748
+ # Terminal endpoint
749
+ terminal_execute_route = url_path_join(base_url, "signalpilot-ai-internal", "terminal", "execute")
750
+
293
751
  handlers = [
294
752
  # Original endpoint
295
753
  (hello_route, HelloWorldHandler),
296
-
754
+
297
755
  # Read all files endpoint
298
756
  (read_all_files_route, ReadAllFilesHandler),
299
-
757
+
758
+ # File scanning endpoints
759
+ (file_scan_route, FileScanHandler),
760
+ (scanned_directories_route, ScannedDirectoriesHandler),
761
+ (select_folder_route, SelectFolderHandler),
762
+ (workdir_route, WorkDirHandler),
763
+ (delete_scanned_directory_route, DeleteScannedDirectoryHandler),
764
+
765
+ # Terminal endpoint
766
+ (terminal_execute_route, TerminalExecuteHandler),
767
+
768
+ # Notebook endpoints
769
+ (notebook_cells_route, NotebookCellsHandler),
770
+
300
771
  # Chat histories endpoints
301
772
  (chat_histories_route, ChatHistoriesHandler),
302
773
  (chat_history_route, ChatHistoriesHandler),
@@ -320,6 +791,9 @@ def setup_handlers(web_app):
320
791
  # Snowflake service endpoints
321
792
  (snowflake_schema_route, SnowflakeSchemaHandler),
322
793
  (snowflake_query_route, SnowflakeQueryHandler),
794
+
795
+ # Notebook HTML export endpoint
796
+ (notebook_html_route, NotebookToHTMLHandler),
323
797
  ]
324
798
 
325
799
  web_app.add_handlers(host_pattern, handlers)
@@ -347,3 +821,5 @@ def setup_handlers(web_app):
347
821
  print(f" - MySQL Query: {mysql_query_route}")
348
822
  print(f" - Snowflake Schema: {snowflake_schema_route}")
349
823
  print(f" - Snowflake Query: {snowflake_query_route}")
824
+ print(f" - Notebook Cells: {notebook_cells_route}")
825
+ print(f" - Notebook to HTML: {notebook_html_route}")
@@ -0,0 +1,23 @@
1
+ # HTML Export Template with Code Toggle
2
+
3
+ This is a custom Jupyter NBConvert HTML template that adds a toggle to hide/show each code cell.
4
+
5
+ ## Usage
6
+
7
+ The handler uses this template automatically. You can modify the notebook cells to control their visibility:
8
+
9
+ - By default, all code cells are hidden with a "Show Code" button
10
+ - To show a code cell by default, tag the cell with 'code_shown' in its metadata
11
+
12
+ ## Template Files
13
+
14
+ - `index.html.j2` - The main template that extends lab/index.html.j2
15
+ - `conf.json` - Configuration for the template
16
+ - `README.md` - This file
17
+
18
+ ## Features
19
+
20
+ - Clean presentation with toggleable code cells
21
+ - Uses JQuery for smooth transitions
22
+ - Removes input/output prompts for cleaner appearance
23
+ - Responsive and modern styling
@@ -0,0 +1,12 @@
1
+ {
2
+ "base_template": "lab",
3
+ "mimetypes": {
4
+ "text/html": true
5
+ },
6
+ "preprocessors": {
7
+ "100-pygments": {
8
+ "type": "nbconvert.preprocessors.CSSHTMLHeaderPreprocessor",
9
+ "enabled": true
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,140 @@
1
+ {% extends 'lab/index.html.j2' %}
2
+
3
+ {% block html_head %}
4
+ {{ super() }}
5
+ <script src="https://code.jquery.com/jquery-3.4.1.min.js"
6
+ integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
7
+ crossorigin="anonymous"></script>
8
+
9
+ <script>
10
+ $(document).ready(function() {
11
+ // Add Show All / Hide All buttons above first code cell (if not already added)
12
+ if ($('.html-toggle-all-buttons').length === 0) {
13
+ // Only add buttons if there is at least one code_shower present
14
+ if ($('.code_shower').length === 0) {
15
+ return;
16
+ }
17
+ var $btnContainer = $('<div class="html-toggle-all-buttons"></div>');
18
+ var $showAllBtn = $('<button type="button" class="html-toggle-btn">Show All Code</button>');
19
+ var $hideAllBtn = $('<button type="button" class="html-toggle-btn">Hide All Code</button>');
20
+ $btnContainer.append($showAllBtn).append($hideAllBtn);
21
+
22
+ // Insert at the top of the <body>
23
+ $('body').prepend($btnContainer);
24
+
25
+ // "Show All" callback
26
+ $showAllBtn.on('click', function() {
27
+ $('.code_shower').each(function() {
28
+ var header = $(this);
29
+ var codecell = header.next();
30
+ // Only toggle if it's hidden
31
+ if (codecell.is(':hidden')) {
32
+ // Update styles and text as necessary
33
+ var wrapper = codecell.parent();
34
+ wrapper.css('display', 'flex');
35
+ wrapper.css('flex-direction', 'column');
36
+ header.css('font-family', 'var(--jp-content-font-family)');
37
+ header.text("Hide Code Cell");
38
+ header.css("border-radius", "2px 2px 0px 0px");
39
+ codecell.slideDown(0);
40
+ }
41
+ });
42
+ });
43
+
44
+ // "Hide All" callback
45
+ $hideAllBtn.on('click', function() {
46
+ $('.code_shower').each(function() {
47
+ var header = $(this);
48
+ var codecell = header.next();
49
+ // Only toggle if it's visible
50
+ if (codecell.is(':visible')) {
51
+ var wrapper = codecell.parent();
52
+ wrapper.css('display', 'flex');
53
+ wrapper.css('flex-direction', 'column');
54
+ header.css('font-family', 'var(--jp-content-font-family)');
55
+ header.text("Show Code Cell");
56
+ header.css("border-radius", "2px 2px 2px 2px");
57
+ codecell.slideUp(0);
58
+ }
59
+ });
60
+ });
61
+ }
62
+
63
+ $('.code_shower').on('click', function() {
64
+ var header = $(this);
65
+ var codecell = $(this).next();
66
+
67
+ // Ensure the codecell's parent is a flex column wrapper
68
+ var wrapper = codecell.parent();
69
+ wrapper.css('display', 'flex');
70
+ wrapper.css('flex-direction', 'column');
71
+
72
+ // Ensure header uses the content font family
73
+ header.css('font-family', 'var(--jp-content-font-family)');
74
+
75
+ codecell.slideToggle(0, function() {
76
+ if (codecell.is(':hidden')) {
77
+ header.text("Show Code Cell");
78
+ header.css("border-radius", "2px 2px 2px 2px");
79
+ } else {
80
+ header.text("Hide Code Cell");
81
+ header.css("border-radius", "2px 2px 0px 0px");
82
+ }
83
+ });
84
+ });
85
+ $('.hidden_default').next().hide();
86
+ });
87
+
88
+ </script>
89
+
90
+ <style>
91
+ div.input {
92
+ flex-direction: column !important;
93
+ }
94
+
95
+ div.input_area {
96
+ border-radius: 0px 0px 2px 2px;
97
+ }
98
+
99
+ div.code_shower {
100
+ background: lightgray;
101
+ padding: 5px 10px;
102
+ cursor: pointer;
103
+ border-radius: 2px 2px 0px 0px;
104
+ font-family: var(--jp-content-font-family);
105
+ }
106
+
107
+ /* Container and button styles for Show/Hide All controls */
108
+ .html-toggle-all-buttons {
109
+ display: flex;
110
+ justify-content: end;
111
+ gap: 8px;
112
+ }
113
+
114
+ .html-toggle-all-buttons .html-toggle-btn {
115
+ background: lightgray;
116
+ padding: 5px 10px;
117
+ cursor: pointer;
118
+ border-radius: 2px;
119
+ border: none;
120
+ font-family: var(--jp-content-font-family);
121
+ }
122
+
123
+ </style>
124
+ {% endblock html_head %}
125
+
126
+ {% block input %}
127
+ {% if 'code_shown' in cell['metadata'].get('tags', []) %}
128
+ <div class="code_shower">Hide Code</div>
129
+ {% else %}
130
+ <div class="code_shower hidden_default">Show Code</div>
131
+ {% endif %}
132
+
133
+ {{ super() }}
134
+ {% endblock input %}
135
+
136
+ {% block output_prompt %}
137
+ {% endblock output_prompt %}
138
+
139
+ {% block in_prompt %}
140
+ {% endblock in_prompt %}