vector-inspector 0.3.7__py3-none-any.whl → 0.3.9__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.
@@ -1,63 +1,29 @@
1
1
  """Updated main window with multi-database support."""
2
2
 
3
3
  from PySide6.QtWidgets import (
4
- QMainWindow,
5
- QWidget,
6
- QVBoxLayout,
7
- QHBoxLayout,
8
- QSplitter,
9
- QTabWidget,
10
- QStatusBar,
11
- QToolBar,
12
4
  QMessageBox,
13
- QInputDialog,
14
5
  QLabel,
15
- QDockWidget,
16
6
  QApplication,
17
7
  QDialog,
8
+ QToolBar,
9
+ QStatusBar,
18
10
  )
19
- from PySide6.QtCore import Qt, Signal, QTimer, QThread
11
+ from PySide6.QtCore import Qt, QTimer, QByteArray
20
12
  from PySide6.QtGui import QAction
21
13
 
22
- from vector_inspector.core.connection_manager import ConnectionManager, ConnectionState
14
+ from vector_inspector.core.connection_manager import ConnectionManager
23
15
  from vector_inspector.core.connections.base_connection import VectorDBConnection
24
- from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
25
- from vector_inspector.core.connections.qdrant_connection import QdrantConnection
26
- from vector_inspector.core.connections.pinecone_connection import PineconeConnection
27
- from vector_inspector.core.connections.pgvector_connection import PgVectorConnection
28
16
  from vector_inspector.services.profile_service import ProfileService
29
17
  from vector_inspector.services.settings_service import SettingsService
18
+ from vector_inspector.ui.main_window_shell import InspectorShell
30
19
  from vector_inspector.ui.components.connection_manager_panel import ConnectionManagerPanel
31
20
  from vector_inspector.ui.components.profile_manager_panel import ProfileManagerPanel
32
- from vector_inspector.ui.views.info_panel import InfoPanel
33
- from vector_inspector.ui.views.metadata_view import MetadataView
34
- from vector_inspector.ui.views.search_view import SearchView
35
- from vector_inspector.ui.components.loading_dialog import LoadingDialog
21
+ from vector_inspector.ui.tabs import InspectorTabs
22
+ from vector_inspector.ui.controllers.connection_controller import ConnectionController
23
+ from vector_inspector.ui.services.dialog_service import DialogService
36
24
 
37
25
 
38
- class ConnectionThread(QThread):
39
- """Background thread for connecting to database."""
40
-
41
- finished = Signal(bool, list, str) # success, collections, error_message
42
-
43
- def __init__(self, connection):
44
- super().__init__()
45
- self.connection = connection
46
-
47
- def run(self):
48
- """Connect to database and get collections."""
49
- try:
50
- success = self.connection.connect()
51
- if success:
52
- collections = self.connection.list_collections()
53
- self.finished.emit(True, collections, "")
54
- else:
55
- self.finished.emit(False, [], "Connection failed")
56
- except Exception as e:
57
- self.finished.emit(False, [], str(e))
58
-
59
-
60
- class MainWindow(QMainWindow):
26
+ class MainWindow(InspectorShell):
61
27
  """Main application window with multi-database support."""
62
28
 
63
29
  def __init__(self):
@@ -67,11 +33,21 @@ class MainWindow(QMainWindow):
67
33
  self.connection_manager = ConnectionManager()
68
34
  self.profile_service = ProfileService()
69
35
  self.settings_service = SettingsService()
70
- self.loading_dialog = LoadingDialog("Loading...", self)
36
+
37
+ # Controller for connection operations
38
+ self.connection_controller = ConnectionController(
39
+ self.connection_manager, self.profile_service, self
40
+ )
71
41
 
72
42
  # State
73
43
  self.visualization_view = None
74
- self._connection_threads = {} # Track connection threads
44
+
45
+ # View references (will be set in _setup_ui)
46
+ self.info_panel = None
47
+ self.metadata_view = None
48
+ self.search_view = None
49
+ self.connection_panel = None
50
+ self.profile_panel = None
75
51
 
76
52
  self.setWindowTitle("Vector Inspector")
77
53
  self.setGeometry(100, 100, 1600, 900)
@@ -82,6 +58,29 @@ class MainWindow(QMainWindow):
82
58
  self._setup_statusbar()
83
59
  self._connect_signals()
84
60
  self._restore_session()
61
+ # Listen for settings changes so updates apply immediately
62
+ try:
63
+ self.settings_service.signals.setting_changed.connect(self._on_setting_changed)
64
+ except Exception:
65
+ pass
66
+ # Restore window geometry if present
67
+ try:
68
+ geom = self.settings_service.get_window_geometry()
69
+ if geom and self.settings_service.get_window_restore_geometry():
70
+ try:
71
+ # restoreGeometry accepts QByteArray; wrap bytes accordingly
72
+ if isinstance(geom, (bytes, bytearray)):
73
+ self.restoreGeometry(QByteArray(geom))
74
+ else:
75
+ self.restoreGeometry(geom)
76
+ except Exception:
77
+ # fallback: try passing raw bytes
78
+ try:
79
+ self.restoreGeometry(geom)
80
+ except Exception:
81
+ pass
82
+ except Exception:
83
+ pass
85
84
  # Show splash after main window is visible
86
85
  QTimer.singleShot(0, self._maybe_show_splash)
87
86
 
@@ -101,62 +100,36 @@ class MainWindow(QMainWindow):
101
100
  print(f"[SplashWindow] Failed to show splash: {e}")
102
101
 
103
102
  def _setup_ui(self):
104
- """Setup the main UI layout."""
105
- # Central widget with splitter
106
- central_widget = QWidget()
107
- self.setCentralWidget(central_widget)
108
-
109
- layout = QHBoxLayout(central_widget)
110
- layout.setContentsMargins(5, 5, 5, 5)
111
-
112
- # Main splitter (left panel | right tabs)
113
- main_splitter = QSplitter(Qt.Horizontal)
114
-
115
- # Left panel - Connections and Profiles
116
- left_panel = QWidget()
117
- left_layout = QVBoxLayout(left_panel)
118
- left_layout.setContentsMargins(0, 0, 0, 0)
119
-
120
- # Create tab widget for connections and profiles
121
- self.left_tabs = QTabWidget()
122
-
123
- # Connection manager panel
103
+ """Setup the main UI layout using InspectorShell."""
104
+ # Left panels - Connections and Profiles
124
105
  self.connection_panel = ConnectionManagerPanel(self.connection_manager)
125
- self.left_tabs.addTab(self.connection_panel, "Active")
106
+ self.add_left_panel(self.connection_panel, "Active")
126
107
 
127
- # Profile manager panel
128
108
  self.profile_panel = ProfileManagerPanel(self.profile_service)
129
- self.left_tabs.addTab(self.profile_panel, "Profiles")
109
+ self.add_left_panel(self.profile_panel, "Profiles")
130
110
 
131
- left_layout.addWidget(self.left_tabs)
111
+ # Main content tabs using TabRegistry
112
+ tab_defs = InspectorTabs.get_standard_tabs()
132
113
 
133
- # Right panel - Tabbed views
134
- self.tab_widget = QTabWidget()
114
+ for i, tab_def in enumerate(tab_defs):
115
+ widget = InspectorTabs.create_tab_widget(tab_def, connection=None)
116
+ self.add_main_tab(widget, tab_def.title)
135
117
 
136
- # Create views (they'll be updated when collection changes)
137
- self.info_panel = InfoPanel(None) # Will be set later
138
- self.metadata_view = MetadataView(None) # Will be set later
139
- self.search_view = SearchView(None) # Will be set later
140
-
141
- self.tab_widget.addTab(self.info_panel, "Info")
142
- self.tab_widget.addTab(self.metadata_view, "Data Browser")
143
- self.tab_widget.addTab(self.search_view, "Search")
144
- self.tab_widget.addTab(QWidget(), "Visualization") # Placeholder
118
+ # Store references to views (except placeholder)
119
+ if i == InspectorTabs.INFO_TAB:
120
+ self.info_panel = widget
121
+ elif i == InspectorTabs.DATA_TAB:
122
+ self.metadata_view = widget
123
+ elif i == InspectorTabs.SEARCH_TAB:
124
+ self.search_view = widget
125
+ # Visualization is lazy-loaded, so it's a placeholder for now
145
126
 
146
127
  # Set Info tab as default
147
- self.tab_widget.setCurrentIndex(0)
128
+ self.set_main_tab_active(InspectorTabs.INFO_TAB)
148
129
 
149
130
  # Connect to tab change to lazy load visualization
150
131
  self.tab_widget.currentChanged.connect(self._on_tab_changed)
151
132
 
152
- # Add panels to splitter
153
- main_splitter.addWidget(left_panel)
154
- main_splitter.addWidget(self.tab_widget)
155
- main_splitter.setStretchFactor(0, 1)
156
- main_splitter.setStretchFactor(1, 4)
157
-
158
- layout.addWidget(main_splitter)
159
-
160
133
  def _setup_menu_bar(self):
161
134
  """Setup application menu bar."""
162
135
  menubar = self.menuBar()
@@ -171,6 +144,13 @@ class MainWindow(QMainWindow):
171
144
 
172
145
  file_menu.addSeparator()
173
146
 
147
+ prefs_action = QAction("Preferences...", self)
148
+ prefs_action.setShortcut("Ctrl+,")
149
+ prefs_action.triggered.connect(self._show_preferences_dialog)
150
+ file_menu.addAction(prefs_action)
151
+
152
+ file_menu.addSeparator()
153
+
174
154
  exit_action = QAction("E&xit", self)
175
155
  exit_action.setShortcut("Ctrl+Q")
176
156
  exit_action.triggered.connect(self.close)
@@ -299,21 +279,67 @@ class MainWindow(QMainWindow):
299
279
 
300
280
  threading.Thread(target=check_updates, daemon=True).start()
301
281
 
282
+ def _show_preferences_dialog(self):
283
+ try:
284
+ from vector_inspector.ui.dialogs.settings_dialog import SettingsDialog
285
+
286
+ dlg = SettingsDialog(self.settings_service, self)
287
+ if dlg.exec() == QDialog.Accepted:
288
+ self._apply_settings_to_views()
289
+ except Exception as e:
290
+ print(f"Failed to open preferences: {e}")
291
+
292
+ def _apply_settings_to_views(self):
293
+ """Apply relevant settings to existing views."""
294
+ try:
295
+ # Breadcrumb visibility
296
+ enabled = self.settings_service.get_breadcrumb_enabled()
297
+ if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
298
+ self.search_view.breadcrumb_label.setVisible(enabled)
299
+ # also set elide mode
300
+ mode = self.settings_service.get_breadcrumb_elide_mode()
301
+ try:
302
+ self.search_view.set_elide_mode(mode)
303
+ except Exception:
304
+ pass
305
+
306
+ # Default results
307
+ default_n = self.settings_service.get_default_n_results()
308
+ if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
309
+ try:
310
+ self.search_view.n_results_spin.setValue(int(default_n))
311
+ except Exception:
312
+ pass
313
+
314
+ except Exception:
315
+ pass
316
+
317
+ def _on_setting_changed(self, key: str, value: object):
318
+ """Handle granular setting change events."""
319
+ try:
320
+ if key == "breadcrumb.enabled":
321
+ enabled = bool(value)
322
+ if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
323
+ self.search_view.breadcrumb_label.setVisible(enabled)
324
+ elif key == "breadcrumb.elide_mode":
325
+ mode = str(value)
326
+ if self.search_view is not None and hasattr(self.search_view, "set_elide_mode"):
327
+ self.search_view.set_elide_mode(mode)
328
+ elif key == "search.default_n_results":
329
+ try:
330
+ n = int(value)
331
+ if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
332
+ self.search_view.n_results_spin.setValue(n)
333
+ except Exception:
334
+ pass
335
+ except Exception:
336
+ pass
337
+
302
338
  def _on_update_indicator_clicked(self, event):
303
339
  # Show update details dialog
304
340
  if not hasattr(self, "_latest_release"):
305
341
  return
306
- from vector_inspector.ui.components.update_details_dialog import UpdateDetailsDialog
307
- from vector_inspector.services.update_service import UpdateService
308
-
309
- latest = self._latest_release
310
- version = latest.get("tag_name", "?")
311
- notes = latest.get("body", "")
312
- instructions = UpdateService.get_update_instructions()
313
- pip_cmd = instructions["pip"]
314
- github_url = instructions["github"]
315
- dlg = UpdateDetailsDialog(version, notes, pip_cmd, github_url, self)
316
- dlg.exec()
342
+ DialogService.show_update_details(self._latest_release, self)
317
343
 
318
344
  def _connect_signals(self):
319
345
  """Connect signals between components."""
@@ -327,6 +353,9 @@ class MainWindow(QMainWindow):
327
353
  self.connection_manager.collections_updated.connect(self._on_collections_updated)
328
354
  self.connection_manager.connection_opened.connect(self._on_connection_opened)
329
355
 
356
+ # Connection controller signals
357
+ self.connection_controller.connection_completed.connect(self._on_connection_completed)
358
+
330
359
  # Connection panel signals
331
360
  self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
332
361
  self.connection_panel.add_connection_btn.clicked.connect(self._new_connection_from_profile)
@@ -334,9 +363,20 @@ class MainWindow(QMainWindow):
334
363
  # Profile panel signals
335
364
  self.profile_panel.connect_profile.connect(self._connect_to_profile)
336
365
 
366
+ def _on_connection_completed(
367
+ self, connection_id: str, success: bool, collections: list, error: str
368
+ ):
369
+ """Handle connection completed event from controller."""
370
+ if success:
371
+ # Switch to Active connections tab
372
+ self.set_left_panel_active(0)
373
+ self.statusBar().showMessage(
374
+ f"Connected successfully ({len(collections)} collections)", 5000
375
+ )
376
+
337
377
  def _on_tab_changed(self, index: int):
338
378
  """Handle tab change - lazy load visualization tab."""
339
- if index == 3 and self.visualization_view is None:
379
+ if index == InspectorTabs.VISUALIZATION_TAB and self.visualization_view is None:
340
380
  # Lazy load visualization view
341
381
  from vector_inspector.ui.views.visualization_view import VisualizationView
342
382
 
@@ -346,9 +386,11 @@ class MainWindow(QMainWindow):
346
386
 
347
387
  self.visualization_view = VisualizationView(conn)
348
388
  # Replace placeholder with actual view
349
- self.tab_widget.removeTab(3)
350
- self.tab_widget.insertTab(3, self.visualization_view, "Visualization")
351
- self.tab_widget.setCurrentIndex(3)
389
+ self.remove_main_tab(InspectorTabs.VISUALIZATION_TAB)
390
+ self.add_main_tab(
391
+ self.visualization_view, "Visualization", InspectorTabs.VISUALIZATION_TAB
392
+ )
393
+ self.set_main_tab_active(InspectorTabs.VISUALIZATION_TAB)
352
394
 
353
395
  # Set collection if one is already selected
354
396
  if active and active.active_collection:
@@ -386,20 +428,22 @@ class MainWindow(QMainWindow):
386
428
  if connection_id == self.connection_manager.get_active_connection_id():
387
429
  # Show loading immediately when collection changes
388
430
  if collection_name:
389
- self.loading_dialog.show_loading(f"Loading collection '{collection_name}'...")
431
+ self.connection_controller.loading_dialog.show_loading(
432
+ f"Loading collection '{collection_name}'..."
433
+ )
390
434
  QApplication.processEvents()
391
435
  try:
392
436
  self._update_views_for_collection(collection_name)
393
437
  finally:
394
- self.loading_dialog.hide_loading()
438
+ self.connection_controller.loading_dialog.hide_loading()
395
439
  else:
396
440
  # Clear collection from views
397
- self.loading_dialog.show_loading("Clearing collection...")
441
+ self.connection_controller.loading_dialog.show_loading("Clearing collection...")
398
442
  QApplication.processEvents()
399
443
  try:
400
444
  self._update_views_for_collection(None)
401
445
  finally:
402
- self.loading_dialog.hide_loading()
446
+ self.connection_controller.loading_dialog.hide_loading()
403
447
 
404
448
  def _on_collections_updated(self, connection_id: str, collections: list):
405
449
  """Handle collections list updated."""
@@ -417,7 +461,9 @@ class MainWindow(QMainWindow):
417
461
  def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
418
462
  """Handle collection selection from connection panel."""
419
463
  # Show loading dialog while switching collections
420
- self.loading_dialog.show_loading(f"Loading collection '{collection_name}'...")
464
+ self.connection_controller.loading_dialog.show_loading(
465
+ f"Loading collection '{collection_name}'..."
466
+ )
421
467
  QApplication.processEvents()
422
468
 
423
469
  try:
@@ -425,7 +471,7 @@ class MainWindow(QMainWindow):
425
471
  # Just update the views
426
472
  self._update_views_for_collection(collection_name)
427
473
  finally:
428
- self.loading_dialog.hide_loading()
474
+ self.connection_controller.loading_dialog.hide_loading()
429
475
 
430
476
  def _update_views_with_connection(self, connection: VectorDBConnection):
431
477
  """Update all views with a new connection."""
@@ -464,176 +510,20 @@ class MainWindow(QMainWindow):
464
510
 
465
511
  def _new_connection_from_profile(self):
466
512
  """Show dialog to create new connection (switches to Profiles tab)."""
467
- self.left_tabs.setCurrentIndex(1) # Switch to Profiles tab
468
- QMessageBox.information(
469
- self,
470
- "Connect to Profile",
471
- "Select a profile from the list and click 'Connect', or click '+' to create a new profile.",
472
- )
513
+ self.set_left_panel_active(1) # Switch to Profiles tab
514
+ DialogService.show_profile_editor_prompt(self)
473
515
 
474
516
  def _show_profile_editor(self):
475
517
  """Show profile editor to create new profile."""
476
- self.left_tabs.setCurrentIndex(1) # Switch to Profiles tab
518
+ self.set_left_panel_active(1) # Switch to Profiles tab
477
519
  self.profile_panel._create_profile()
478
520
 
479
521
  def _connect_to_profile(self, profile_id: str):
480
- """Connect to a profile."""
481
- profile_data = self.profile_service.get_profile_with_credentials(profile_id)
482
- if not profile_data:
483
- QMessageBox.warning(self, "Error", "Profile not found.")
484
- return
485
-
486
- # Check connection limit
487
- if self.connection_manager.get_connection_count() >= ConnectionManager.MAX_CONNECTIONS:
488
- QMessageBox.warning(
489
- self,
490
- "Connection Limit",
491
- f"Maximum number of connections ({ConnectionManager.MAX_CONNECTIONS}) reached. "
492
- "Please close a connection first.",
493
- )
494
- return
495
-
496
- # Create connection
497
- provider = profile_data["provider"]
498
- config = profile_data["config"]
499
- credentials = profile_data.get("credentials", {})
500
-
501
- try:
502
- # Create connection object
503
- if provider == "chromadb":
504
- connection = self._create_chroma_connection(config, credentials)
505
- elif provider == "qdrant":
506
- connection = self._create_qdrant_connection(config, credentials)
507
- elif provider == "pinecone":
508
- connection = self._create_pinecone_connection(config, credentials)
509
- elif provider == "pgvector":
510
- connection = self._create_pgvector_connection(config, credentials)
511
- else:
512
- QMessageBox.warning(self, "Error", f"Unsupported provider: {provider}")
513
- return
514
-
515
- # Register with connection manager, using profile_id as connection_id for persistence
516
- connection_id = self.connection_manager.create_connection(
517
- name=profile_data["name"],
518
- provider=provider,
519
- connection=connection,
520
- config=config,
521
- connection_id=profile_data["id"],
522
- )
523
-
524
- # Update state to connecting
525
- self.connection_manager.update_connection_state(
526
- connection_id, ConnectionState.CONNECTING
527
- )
528
-
529
- # Connect in background thread
530
- thread = ConnectionThread(connection)
531
- thread.finished.connect(
532
- lambda success, collections, error: self._on_connection_finished(
533
- connection_id, success, collections, error
534
- )
535
- )
536
- self._connection_threads[connection_id] = thread
537
- thread.start()
538
-
539
- # Show loading dialog
540
- self.loading_dialog.show_loading(f"Connecting to {profile_data['name']}...")
541
-
542
- except Exception as e:
543
- QMessageBox.critical(self, "Connection Error", f"Failed to create connection: {e}")
544
-
545
- def _create_chroma_connection(self, config: dict, credentials: dict) -> ChromaDBConnection:
546
- """Create a ChromaDB connection."""
547
- conn_type = config.get("type")
548
-
549
- if conn_type == "persistent":
550
- return ChromaDBConnection(path=config.get("path"))
551
- elif conn_type == "http":
552
- return ChromaDBConnection(host=config.get("host"), port=config.get("port"))
553
- else: # ephemeral
554
- return ChromaDBConnection()
555
-
556
- def _create_qdrant_connection(self, config: dict, credentials: dict) -> QdrantConnection:
557
- """Create a Qdrant connection."""
558
- conn_type = config.get("type")
559
- api_key = credentials.get("api_key")
560
-
561
- if conn_type == "persistent":
562
- return QdrantConnection(path=config.get("path"))
563
- elif conn_type == "http":
564
- return QdrantConnection(
565
- host=config.get("host"), port=config.get("port"), api_key=api_key
566
- )
567
- else: # ephemeral
568
- return QdrantConnection()
569
-
570
- def _create_pinecone_connection(self, config: dict, credentials: dict) -> PineconeConnection:
571
- """Create a Pinecone connection."""
572
- api_key = credentials.get("api_key")
573
- if not api_key:
574
- raise ValueError("Pinecone requires an API key")
575
-
576
- return PineconeConnection(api_key=api_key)
577
-
578
- def _create_pgvector_connection(self, config: dict, credentials: dict) -> PgVectorConnection:
579
- """Create a PgVector/Postgres connection from profile config/credentials."""
580
- conn_type = config.get("type")
581
-
582
- # We expect HTTP-style profile for pgvector (host/port + db creds)
583
- if conn_type == "http":
584
- host = config.get("host", "localhost")
585
- port = int(config.get("port", 5432))
586
- database = config.get("database")
587
- user = config.get("user")
588
- # Prefer password from credentials
589
- password = credentials.get("password")
590
-
591
- return PgVectorConnection(
592
- host=host, port=port, database=database, user=user, password=password
593
- )
594
-
595
- raise ValueError("Unsupported connection type for PgVector profile")
596
-
597
- def _on_connection_finished(
598
- self, connection_id: str, success: bool, collections: list, error: str
599
- ):
600
- """Handle connection thread completion."""
601
- self.loading_dialog.hide_loading()
602
-
603
- # Clean up thread
604
- thread = self._connection_threads.pop(connection_id, None)
605
- if thread:
606
- thread.wait() # Wait for thread to fully finish
607
- thread.deleteLater()
608
-
522
+ """Connect to a profile using the connection controller."""
523
+ success = self.connection_controller.connect_to_profile(profile_id)
609
524
  if success:
610
- # Update state to connected
611
- self.connection_manager.update_connection_state(
612
- connection_id, ConnectionState.CONNECTED
613
- )
614
-
615
- # Mark connection as opened first (will show in UI)
616
- self.connection_manager.mark_connection_opened(connection_id)
617
-
618
- # Then update collections (UI item now exists to receive them)
619
- self.connection_manager.update_collections(connection_id, collections)
620
-
621
- # Switch to Active connections tab
622
- self.left_tabs.setCurrentIndex(0)
623
-
624
- self.statusBar().showMessage(
625
- f"Connected successfully ({len(collections)} collections)", 5000
626
- )
627
- else:
628
- # Update state to error
629
- self.connection_manager.update_connection_state(
630
- connection_id, ConnectionState.ERROR, error
631
- )
632
-
633
- QMessageBox.warning(self, "Connection Failed", f"Failed to connect: {error}")
634
-
635
- # Remove the failed connection
636
- self.connection_manager.close_connection(connection_id)
525
+ # Switch to Active connections tab after initiating connection
526
+ self.set_left_panel_active(0)
637
527
 
638
528
  def _refresh_active_connection(self):
639
529
  """Refresh collections for the active connection."""
@@ -663,15 +553,12 @@ class MainWindow(QMainWindow):
663
553
  10000,
664
554
  )
665
555
 
556
+ # Apply settings to views after UI is built
557
+ self._apply_settings_to_views()
558
+
666
559
  def _show_about(self):
667
560
  """Show about dialog."""
668
- from .main_window import get_about_html
669
-
670
- QMessageBox.about(
671
- self,
672
- "About Vector Inspector",
673
- get_about_html(),
674
- )
561
+ DialogService.show_about(self)
675
562
 
676
563
  def _toggle_cache(self, checked: bool):
677
564
  """Toggle caching on/off."""
@@ -681,53 +568,54 @@ class MainWindow(QMainWindow):
681
568
 
682
569
  def _show_migration_dialog(self):
683
570
  """Show cross-database migration dialog."""
684
- if self.connection_manager.get_connection_count() < 2:
685
- QMessageBox.information(
686
- self,
687
- "Insufficient Connections",
688
- "You need at least 2 active connections to migrate data.\n"
689
- "Please connect to additional databases first.",
690
- )
691
- return
692
-
693
- from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
694
-
695
- dialog = CrossDatabaseMigrationDialog(self.connection_manager, self)
696
- dialog.exec()
571
+ DialogService.show_migration_dialog(self.connection_manager, self)
697
572
 
698
573
  def _show_backup_restore_dialog(self):
699
574
  """Show backup/restore dialog for the active collection."""
700
- # Check if there's an active connection
575
+ # Get active connection and collection
701
576
  connection = self.connection_manager.get_active_connection()
702
- if not connection:
703
- QMessageBox.information(self, "No Connection", "Please connect to a database first.")
704
- return
705
-
706
- # Get active collection
707
577
  collection_name = self.connection_manager.get_active_collection()
708
- if not collection_name:
709
- # Allow opening dialog without a collection selected (for restore-only)
710
- QMessageBox.information(
711
- self,
712
- "No Collection Selected",
713
- "You can restore backups without a collection selected.\n"
714
- "To create a backup, please select a collection first.",
715
- )
716
578
 
717
- from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
579
+ # Show dialog
580
+ result = DialogService.show_backup_restore_dialog(connection, collection_name or "", self)
718
581
 
719
- dialog = BackupRestoreDialog(connection, collection_name or "", self)
720
- if dialog.exec() == QDialog.Accepted:
582
+ if result == QDialog.Accepted:
721
583
  # Refresh collections after restore
722
584
  self._refresh_active_connection()
723
585
 
586
+ def show_search_results(self, collection_name: str, results: dict, context_info: str = ""):
587
+ """Display search results in the Search tab.
588
+
589
+ This is an extension point that allows external code (e.g., pro features)
590
+ to programmatically display search results.
591
+
592
+ Args:
593
+ collection_name: Name of the collection
594
+ results: Search results dictionary
595
+ context_info: Optional context string (e.g., "Similar to: item_123")
596
+ """
597
+ # Switch to search tab
598
+ self.set_main_tab_active(InspectorTabs.SEARCH_TAB)
599
+
600
+ # Set the collection if needed
601
+ if self.search_view.current_collection != collection_name:
602
+ active = self.connection_manager.get_active_connection()
603
+ database_name = active.id if active else ""
604
+ self.search_view.set_collection(collection_name, database_name)
605
+
606
+ # Display the results
607
+ self.search_view.search_results = results
608
+ self.search_view._display_results(results)
609
+
610
+ # Update status with context if provided
611
+ if context_info:
612
+ num_results = len(results.get("ids", [[]])[0])
613
+ self.search_view.results_status.setText(f"{context_info} - Found {num_results} results")
614
+
724
615
  def closeEvent(self, event):
725
616
  """Handle application close."""
726
- # Wait for all connection threads to finish
727
- for thread in list(self._connection_threads.values()):
728
- if thread.isRunning():
729
- thread.quit()
730
- thread.wait(1000) # Wait up to 1 second
617
+ # Clean up connection controller
618
+ self.connection_controller.cleanup()
731
619
 
732
620
  # Clean up temp HTML files from visualization view
733
621
  if self.visualization_view is not None:
@@ -738,21 +626,23 @@ class MainWindow(QMainWindow):
738
626
  # Close all connections
739
627
  self.connection_manager.close_all_connections()
740
628
 
741
- event.accept()
742
-
629
+ # Save window geometry if enabled
630
+ try:
631
+ if self.settings_service.get_window_restore_geometry():
632
+ geom = self.saveGeometry()
633
+ # geom may be a QByteArray; convert to raw bytes
634
+ try:
635
+ if isinstance(geom, QByteArray):
636
+ b = bytes(geom)
637
+ else:
638
+ b = bytes(geom)
639
+ self.settings_service.set_window_geometry(b)
640
+ except Exception:
641
+ try:
642
+ self.settings_service.set_window_geometry(bytes(geom))
643
+ except Exception:
644
+ pass
645
+ except Exception:
646
+ pass
743
647
 
744
- def get_about_html():
745
- from vector_inspector.utils.version import get_app_version
746
-
747
- version = get_app_version()
748
- version_html = (
749
- f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
750
- )
751
- return (
752
- version_html + "<p>A comprehensive desktop application for visualizing, "
753
- "querying, and managing multiple vector databases simultaneously.</p>"
754
- '<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
755
- "<hr />"
756
- "<p>Built with PySide6</p>"
757
- "<p><b>New:</b> Pinecone support!</p>"
758
- )
648
+ event.accept()