vector-inspector 0.3.2__py3-none-any.whl → 0.3.3__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 (32) hide show
  1. vector_inspector/core/connection_manager.py +55 -49
  2. vector_inspector/core/connections/base_connection.py +41 -41
  3. vector_inspector/core/connections/chroma_connection.py +110 -86
  4. vector_inspector/core/connections/pinecone_connection.py +168 -182
  5. vector_inspector/core/connections/qdrant_connection.py +109 -126
  6. vector_inspector/core/connections/qdrant_helpers/__init__.py +4 -0
  7. vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py +35 -0
  8. vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py +51 -0
  9. vector_inspector/core/connections/template_connection.py +55 -65
  10. vector_inspector/core/embedding_utils.py +32 -32
  11. vector_inspector/core/logging.py +27 -0
  12. vector_inspector/core/model_registry.py +4 -3
  13. vector_inspector/main.py +6 -2
  14. vector_inspector/services/backup_helpers.py +63 -0
  15. vector_inspector/services/backup_restore_service.py +73 -152
  16. vector_inspector/services/credential_service.py +33 -40
  17. vector_inspector/services/import_export_service.py +70 -67
  18. vector_inspector/services/profile_service.py +92 -94
  19. vector_inspector/services/settings_service.py +68 -48
  20. vector_inspector/services/visualization_service.py +40 -39
  21. vector_inspector/ui/components/splash_window.py +57 -0
  22. vector_inspector/ui/dialogs/cross_db_migration.py +6 -5
  23. vector_inspector/ui/main_window.py +200 -146
  24. vector_inspector/ui/views/info_panel.py +208 -127
  25. vector_inspector/ui/views/metadata_view.py +8 -7
  26. vector_inspector/ui/views/search_view.py +97 -75
  27. vector_inspector/ui/views/visualization_view.py +140 -97
  28. vector_inspector/utils/version.py +5 -0
  29. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.3.dist-info}/METADATA +3 -1
  30. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.3.dist-info}/RECORD +32 -25
  31. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.3.dist-info}/WHEEL +0 -0
  32. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.3.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,20 @@
1
1
  """Updated main window with multi-database support."""
2
2
 
3
3
  from PySide6.QtWidgets import (
4
- QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
5
- QSplitter, QTabWidget, QStatusBar, QToolBar,
6
- QMessageBox, QInputDialog, QLabel, QDockWidget, QApplication, QDialog
4
+ QMainWindow,
5
+ QWidget,
6
+ QVBoxLayout,
7
+ QHBoxLayout,
8
+ QSplitter,
9
+ QTabWidget,
10
+ QStatusBar,
11
+ QToolBar,
12
+ QMessageBox,
13
+ QInputDialog,
14
+ QLabel,
15
+ QDockWidget,
16
+ QApplication,
17
+ QDialog,
7
18
  )
8
19
  from PySide6.QtCore import Qt, Signal, QTimer, QThread
9
20
  from PySide6.QtGui import QAction
@@ -25,13 +36,13 @@ from vector_inspector.ui.components.loading_dialog import LoadingDialog
25
36
 
26
37
  class ConnectionThread(QThread):
27
38
  """Background thread for connecting to database."""
28
-
39
+
29
40
  finished = Signal(bool, list, str) # success, collections, error_message
30
-
41
+
31
42
  def __init__(self, connection):
32
43
  super().__init__()
33
44
  self.connection = connection
34
-
45
+
35
46
  def run(self):
36
47
  """Connect to database and get collections."""
37
48
  try:
@@ -47,208 +58,229 @@ class ConnectionThread(QThread):
47
58
 
48
59
  class MainWindow(QMainWindow):
49
60
  """Main application window with multi-database support."""
50
-
61
+
51
62
  def __init__(self):
52
63
  super().__init__()
53
-
64
+
54
65
  # Core services
55
66
  self.connection_manager = ConnectionManager()
56
67
  self.profile_service = ProfileService()
57
68
  self.settings_service = SettingsService()
58
69
  self.loading_dialog = LoadingDialog("Loading...", self)
59
-
70
+
60
71
  # State
61
72
  self.visualization_view = None
62
73
  self._connection_threads = {} # Track connection threads
63
-
74
+
64
75
  self.setWindowTitle("Vector Inspector")
65
76
  self.setGeometry(100, 100, 1600, 900)
66
-
77
+
67
78
  self._setup_ui()
68
79
  self._setup_menu_bar()
69
80
  self._setup_toolbar()
70
81
  self._setup_statusbar()
71
82
  self._connect_signals()
72
83
  self._restore_session()
73
-
84
+ # Show splash after main window is visible
85
+ QTimer.singleShot(0, self._maybe_show_splash)
86
+
87
+ def _maybe_show_splash(self):
88
+ # Only show splash if not hidden in settings
89
+ if not self.settings_service.get("hide_splash_window", False):
90
+ try:
91
+ from vector_inspector.ui.components.splash_window import SplashWindow
92
+
93
+ splash = SplashWindow(self)
94
+ splash.setWindowModality(Qt.ApplicationModal)
95
+ splash.raise_()
96
+ splash.activateWindow()
97
+ if splash.exec() == QDialog.Accepted and splash.should_hide():
98
+ self.settings_service.set("hide_splash_window", True)
99
+ except Exception as e:
100
+ print(f"[SplashWindow] Failed to show splash: {e}")
101
+
74
102
  def _setup_ui(self):
75
103
  """Setup the main UI layout."""
76
104
  # Central widget with splitter
77
105
  central_widget = QWidget()
78
106
  self.setCentralWidget(central_widget)
79
-
107
+
80
108
  layout = QHBoxLayout(central_widget)
81
109
  layout.setContentsMargins(5, 5, 5, 5)
82
-
110
+
83
111
  # Main splitter (left panel | right tabs)
84
112
  main_splitter = QSplitter(Qt.Horizontal)
85
-
113
+
86
114
  # Left panel - Connections and Profiles
87
115
  left_panel = QWidget()
88
116
  left_layout = QVBoxLayout(left_panel)
89
117
  left_layout.setContentsMargins(0, 0, 0, 0)
90
-
118
+
91
119
  # Create tab widget for connections and profiles
92
120
  self.left_tabs = QTabWidget()
93
-
121
+
94
122
  # Connection manager panel
95
123
  self.connection_panel = ConnectionManagerPanel(self.connection_manager)
96
124
  self.left_tabs.addTab(self.connection_panel, "Active")
97
-
125
+
98
126
  # Profile manager panel
99
127
  self.profile_panel = ProfileManagerPanel(self.profile_service)
100
128
  self.left_tabs.addTab(self.profile_panel, "Profiles")
101
-
129
+
102
130
  left_layout.addWidget(self.left_tabs)
103
-
131
+
104
132
  # Right panel - Tabbed views
105
133
  self.tab_widget = QTabWidget()
106
-
134
+
107
135
  # Create views (they'll be updated when collection changes)
108
136
  self.info_panel = InfoPanel(None) # Will be set later
109
137
  self.metadata_view = MetadataView(None) # Will be set later
110
138
  self.search_view = SearchView(None) # Will be set later
111
-
139
+
112
140
  self.tab_widget.addTab(self.info_panel, "Info")
113
141
  self.tab_widget.addTab(self.metadata_view, "Data Browser")
114
142
  self.tab_widget.addTab(self.search_view, "Search")
115
143
  self.tab_widget.addTab(QWidget(), "Visualization") # Placeholder
116
-
144
+
117
145
  # Set Info tab as default
118
146
  self.tab_widget.setCurrentIndex(0)
119
-
147
+
120
148
  # Connect to tab change to lazy load visualization
121
149
  self.tab_widget.currentChanged.connect(self._on_tab_changed)
122
-
150
+
123
151
  # Add panels to splitter
124
152
  main_splitter.addWidget(left_panel)
125
153
  main_splitter.addWidget(self.tab_widget)
126
154
  main_splitter.setStretchFactor(0, 1)
127
155
  main_splitter.setStretchFactor(1, 4)
128
-
156
+
129
157
  layout.addWidget(main_splitter)
130
-
158
+
131
159
  def _setup_menu_bar(self):
132
160
  """Setup application menu bar."""
133
161
  menubar = self.menuBar()
134
-
162
+
135
163
  # File menu
136
164
  file_menu = menubar.addMenu("&File")
137
-
165
+
138
166
  new_connection_action = QAction("&New Connection...", self)
139
167
  new_connection_action.setShortcut("Ctrl+N")
140
168
  new_connection_action.triggered.connect(self._new_connection_from_profile)
141
169
  file_menu.addAction(new_connection_action)
142
-
170
+
143
171
  file_menu.addSeparator()
144
-
172
+
145
173
  exit_action = QAction("E&xit", self)
146
174
  exit_action.setShortcut("Ctrl+Q")
147
175
  exit_action.triggered.connect(self.close)
148
176
  file_menu.addAction(exit_action)
149
-
177
+
150
178
  # Connection menu
151
179
  connection_menu = menubar.addMenu("&Connection")
152
-
180
+
153
181
  new_profile_action = QAction("New &Profile...", self)
154
182
  new_profile_action.triggered.connect(self._show_profile_editor)
155
183
  connection_menu.addAction(new_profile_action)
156
-
184
+
157
185
  connection_menu.addSeparator()
158
-
186
+
159
187
  refresh_action = QAction("&Refresh Collections", self)
160
188
  refresh_action.setShortcut("F5")
161
189
  refresh_action.triggered.connect(self._refresh_active_connection)
162
190
  connection_menu.addAction(refresh_action)
163
-
191
+
164
192
  connection_menu.addSeparator()
165
-
193
+
166
194
  backup_action = QAction("&Backup/Restore...", self)
167
195
  backup_action.triggered.connect(self._show_backup_restore_dialog)
168
196
  connection_menu.addAction(backup_action)
169
-
197
+
170
198
  migrate_action = QAction("&Migrate Data...", self)
171
199
  migrate_action.triggered.connect(self._show_migration_dialog)
172
200
  connection_menu.addAction(migrate_action)
173
-
201
+
174
202
  # View menu
175
203
  view_menu = menubar.addMenu("&View")
176
-
204
+
177
205
  self.cache_action = QAction("Enable &Caching", self)
178
206
  self.cache_action.setCheckable(True)
179
207
  self.cache_action.setChecked(self.settings_service.get_cache_enabled())
180
208
  self.cache_action.triggered.connect(self._toggle_cache)
181
209
  view_menu.addAction(self.cache_action)
182
-
210
+
183
211
  # Help menu
184
212
  help_menu = menubar.addMenu("&Help")
185
-
213
+
186
214
  about_action = QAction("&About", self)
187
215
  about_action.triggered.connect(self._show_about)
188
216
  help_menu.addAction(about_action)
189
-
217
+
190
218
  def _setup_toolbar(self):
191
219
  """Setup application toolbar."""
192
220
  toolbar = QToolBar("Main Toolbar")
193
221
  toolbar.setMovable(False)
194
222
  self.addToolBar(toolbar)
195
-
223
+
196
224
  new_connection_action = QAction("New Connection", self)
197
225
  new_connection_action.triggered.connect(self._new_connection_from_profile)
198
226
  toolbar.addAction(new_connection_action)
199
-
227
+
200
228
  toolbar.addSeparator()
201
-
229
+
202
230
  refresh_action = QAction("Refresh", self)
203
231
  refresh_action.triggered.connect(self._refresh_active_connection)
204
232
  toolbar.addAction(refresh_action)
205
-
233
+
206
234
  def _setup_statusbar(self):
207
235
  """Setup status bar with connection breadcrumb."""
208
236
  status_bar = QStatusBar()
209
237
  self.setStatusBar(status_bar)
210
-
238
+
211
239
  # Breadcrumb label
212
240
  self.breadcrumb_label = QLabel("No active connection")
213
241
  self.statusBar().addPermanentWidget(self.breadcrumb_label)
214
-
242
+
215
243
  self.statusBar().showMessage("Ready")
216
-
244
+
217
245
  def _connect_signals(self):
218
246
  """Connect signals between components."""
219
247
  # Connection manager signals
220
- self.connection_manager.active_connection_changed.connect(self._on_active_connection_changed)
221
- self.connection_manager.active_collection_changed.connect(self._on_active_collection_changed)
248
+ self.connection_manager.active_connection_changed.connect(
249
+ self._on_active_connection_changed
250
+ )
251
+ self.connection_manager.active_collection_changed.connect(
252
+ self._on_active_collection_changed
253
+ )
222
254
  self.connection_manager.collections_updated.connect(self._on_collections_updated)
223
255
  self.connection_manager.connection_opened.connect(self._on_connection_opened)
224
-
256
+
225
257
  # Connection panel signals
226
258
  self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
227
259
  self.connection_panel.add_connection_btn.clicked.connect(self._new_connection_from_profile)
228
-
260
+
229
261
  # Profile panel signals
230
262
  self.profile_panel.connect_profile.connect(self._connect_to_profile)
231
-
263
+
232
264
  def _on_tab_changed(self, index: int):
233
265
  """Handle tab change - lazy load visualization tab."""
234
266
  if index == 3 and self.visualization_view is None:
235
267
  # Lazy load visualization view
236
268
  from vector_inspector.ui.views.visualization_view import VisualizationView
237
-
269
+
238
270
  # Get active connection
239
271
  active = self.connection_manager.get_active_connection()
240
272
  conn = active.connection if active else None
241
-
273
+
242
274
  self.visualization_view = VisualizationView(conn)
243
275
  # Replace placeholder with actual view
244
276
  self.tab_widget.removeTab(3)
245
277
  self.tab_widget.insertTab(3, self.visualization_view, "Visualization")
246
278
  self.tab_widget.setCurrentIndex(3)
247
-
279
+
248
280
  # Set collection if one is already selected
249
281
  if active and active.active_collection:
250
282
  self.visualization_view.set_collection(active.active_collection)
251
-
283
+
252
284
  def _on_active_connection_changed(self, connection_id):
253
285
  """Handle active connection change."""
254
286
  if connection_id:
@@ -256,10 +288,10 @@ class MainWindow(QMainWindow):
256
288
  if instance:
257
289
  # Update breadcrumb
258
290
  self.breadcrumb_label.setText(instance.get_breadcrumb())
259
-
291
+
260
292
  # Update all views with new connection
261
293
  self._update_views_with_connection(instance.connection)
262
-
294
+
263
295
  # If there's an active collection, update views with it
264
296
  if instance.active_collection:
265
297
  self._update_views_for_collection(instance.active_collection)
@@ -269,14 +301,14 @@ class MainWindow(QMainWindow):
269
301
  else:
270
302
  self.breadcrumb_label.setText("No active connection")
271
303
  self._update_views_with_connection(None)
272
-
273
- def _on_active_collection_changed(self, connection_id: str, collection_name):
304
+
305
+ def _on_active_collection_changed(self, connection_id: str, collection_name: str):
274
306
  """Handle active collection change."""
275
307
  instance = self.connection_manager.get_connection(connection_id)
276
308
  if instance:
277
309
  # Update breadcrumb
278
310
  self.breadcrumb_label.setText(instance.get_breadcrumb())
279
-
311
+
280
312
  # Update views if this is the active connection
281
313
  if connection_id == self.connection_manager.get_active_connection_id():
282
314
  # Show loading immediately when collection changes
@@ -295,12 +327,12 @@ class MainWindow(QMainWindow):
295
327
  self._update_views_for_collection(None)
296
328
  finally:
297
329
  self.loading_dialog.hide_loading()
298
-
330
+
299
331
  def _on_collections_updated(self, connection_id: str, collections: list):
300
332
  """Handle collections list updated."""
301
333
  # UI automatically updates via connection_manager_panel
302
334
  pass
303
-
335
+
304
336
  def _on_connection_opened(self, connection_id: str):
305
337
  """Handle connection successfully opened."""
306
338
  # If this is the active connection, refresh the info panel
@@ -308,20 +340,20 @@ class MainWindow(QMainWindow):
308
340
  instance = self.connection_manager.get_connection(connection_id)
309
341
  if instance and instance.connection:
310
342
  self.info_panel.refresh_database_info()
311
-
343
+
312
344
  def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
313
345
  """Handle collection selection from connection panel."""
314
346
  # Show loading dialog while switching collections
315
347
  self.loading_dialog.show_loading(f"Loading collection '{collection_name}'...")
316
348
  QApplication.processEvents()
317
-
349
+
318
350
  try:
319
351
  # The connection manager already handled setting active collection
320
352
  # Just update the views
321
353
  self._update_views_for_collection(collection_name)
322
354
  finally:
323
355
  self.loading_dialog.hide_loading()
324
-
356
+
325
357
  def _update_views_with_connection(self, connection: VectorDBConnection):
326
358
  """Update all views with a new connection."""
327
359
  # Clear current collection when switching connections
@@ -330,69 +362,69 @@ class MainWindow(QMainWindow):
330
362
  self.search_view.current_collection = None
331
363
  if self.visualization_view is not None:
332
364
  self.visualization_view.current_collection = None
333
-
365
+
334
366
  # Update connection references
335
367
  self.info_panel.connection = connection
336
368
  self.metadata_view.connection = connection
337
369
  self.search_view.connection = connection
338
-
370
+
339
371
  if self.visualization_view is not None:
340
372
  self.visualization_view.connection = connection
341
-
373
+
342
374
  # Refresh info panel (will show no collection selected)
343
375
  if connection:
344
376
  self.info_panel.refresh_database_info()
345
-
377
+
346
378
  def _update_views_for_collection(self, collection_name: str):
347
379
  """Update all views with the selected collection."""
348
380
  if collection_name:
349
381
  # Get active connection ID to use as database identifier
350
382
  active = self.connection_manager.get_active_connection()
351
383
  database_name = active.id if active else ""
352
-
384
+
353
385
  self.info_panel.set_collection(collection_name, database_name)
354
386
  self.metadata_view.set_collection(collection_name, database_name)
355
387
  self.search_view.set_collection(collection_name, database_name)
356
-
388
+
357
389
  if self.visualization_view is not None:
358
390
  self.visualization_view.set_collection(collection_name)
359
-
391
+
360
392
  def _new_connection_from_profile(self):
361
393
  """Show dialog to create new connection (switches to Profiles tab)."""
362
394
  self.left_tabs.setCurrentIndex(1) # Switch to Profiles tab
363
395
  QMessageBox.information(
364
396
  self,
365
397
  "Connect to Profile",
366
- "Select a profile from the list and click 'Connect', or click '+' to create a new profile."
398
+ "Select a profile from the list and click 'Connect', or click '+' to create a new profile.",
367
399
  )
368
-
400
+
369
401
  def _show_profile_editor(self):
370
402
  """Show profile editor to create new profile."""
371
403
  self.left_tabs.setCurrentIndex(1) # Switch to Profiles tab
372
404
  self.profile_panel._create_profile()
373
-
405
+
374
406
  def _connect_to_profile(self, profile_id: str):
375
407
  """Connect to a profile."""
376
408
  profile_data = self.profile_service.get_profile_with_credentials(profile_id)
377
409
  if not profile_data:
378
410
  QMessageBox.warning(self, "Error", "Profile not found.")
379
411
  return
380
-
412
+
381
413
  # Check connection limit
382
414
  if self.connection_manager.get_connection_count() >= ConnectionManager.MAX_CONNECTIONS:
383
415
  QMessageBox.warning(
384
416
  self,
385
417
  "Connection Limit",
386
418
  f"Maximum number of connections ({ConnectionManager.MAX_CONNECTIONS}) reached. "
387
- "Please close a connection first."
419
+ "Please close a connection first.",
388
420
  )
389
421
  return
390
-
422
+
391
423
  # Create connection
392
424
  provider = profile_data["provider"]
393
425
  config = profile_data["config"]
394
426
  credentials = profile_data.get("credentials", {})
395
-
427
+
396
428
  try:
397
429
  # Create connection object
398
430
  if provider == "chromadb":
@@ -404,18 +436,21 @@ class MainWindow(QMainWindow):
404
436
  else:
405
437
  QMessageBox.warning(self, "Error", f"Unsupported provider: {provider}")
406
438
  return
407
-
408
- # Register with connection manager
439
+
440
+ # Register with connection manager, using profile_id as connection_id for persistence
409
441
  connection_id = self.connection_manager.create_connection(
410
442
  name=profile_data["name"],
411
443
  provider=provider,
412
444
  connection=connection,
413
- config=config
445
+ config=config,
446
+ connection_id=profile_data["id"],
414
447
  )
415
-
448
+
416
449
  # Update state to connecting
417
- self.connection_manager.update_connection_state(connection_id, ConnectionState.CONNECTING)
418
-
450
+ self.connection_manager.update_connection_state(
451
+ connection_id, ConnectionState.CONNECTING
452
+ )
453
+
419
454
  # Connect in background thread
420
455
  thread = ConnectionThread(connection)
421
456
  thread.finished.connect(
@@ -425,101 +460,104 @@ class MainWindow(QMainWindow):
425
460
  )
426
461
  self._connection_threads[connection_id] = thread
427
462
  thread.start()
428
-
463
+
429
464
  # Show loading dialog
430
465
  self.loading_dialog.show_loading(f"Connecting to {profile_data['name']}...")
431
-
466
+
432
467
  except Exception as e:
433
468
  QMessageBox.critical(self, "Connection Error", f"Failed to create connection: {e}")
434
-
469
+
435
470
  def _create_chroma_connection(self, config: dict, credentials: dict) -> ChromaDBConnection:
436
471
  """Create a ChromaDB connection."""
437
472
  conn_type = config.get("type")
438
-
473
+
439
474
  if conn_type == "persistent":
440
475
  return ChromaDBConnection(path=config.get("path"))
441
476
  elif conn_type == "http":
442
- return ChromaDBConnection(
443
- host=config.get("host"),
444
- port=config.get("port")
445
- )
477
+ return ChromaDBConnection(host=config.get("host"), port=config.get("port"))
446
478
  else: # ephemeral
447
479
  return ChromaDBConnection()
448
-
480
+
449
481
  def _create_qdrant_connection(self, config: dict, credentials: dict) -> QdrantConnection:
450
482
  """Create a Qdrant connection."""
451
483
  conn_type = config.get("type")
452
484
  api_key = credentials.get("api_key")
453
-
485
+
454
486
  if conn_type == "persistent":
455
487
  return QdrantConnection(path=config.get("path"))
456
488
  elif conn_type == "http":
457
489
  return QdrantConnection(
458
- host=config.get("host"),
459
- port=config.get("port"),
460
- api_key=api_key
490
+ host=config.get("host"), port=config.get("port"), api_key=api_key
461
491
  )
462
492
  else: # ephemeral
463
493
  return QdrantConnection()
464
-
494
+
465
495
  def _create_pinecone_connection(self, config: dict, credentials: dict) -> PineconeConnection:
466
496
  """Create a Pinecone connection."""
467
497
  api_key = credentials.get("api_key")
468
498
  if not api_key:
469
499
  raise ValueError("Pinecone requires an API key")
470
-
500
+
471
501
  return PineconeConnection(api_key=api_key)
472
-
473
- def _on_connection_finished(self, connection_id: str, success: bool, collections: list, error: str):
502
+
503
+ def _on_connection_finished(
504
+ self, connection_id: str, success: bool, collections: list, error: str
505
+ ):
474
506
  """Handle connection thread completion."""
475
507
  self.loading_dialog.hide_loading()
476
-
508
+
477
509
  # Clean up thread
478
510
  thread = self._connection_threads.pop(connection_id, None)
479
511
  if thread:
480
512
  thread.wait() # Wait for thread to fully finish
481
513
  thread.deleteLater()
482
-
514
+
483
515
  if success:
484
516
  # Update state to connected
485
- self.connection_manager.update_connection_state(connection_id, ConnectionState.CONNECTED)
486
-
517
+ self.connection_manager.update_connection_state(
518
+ connection_id, ConnectionState.CONNECTED
519
+ )
520
+
487
521
  # Mark connection as opened first (will show in UI)
488
522
  self.connection_manager.mark_connection_opened(connection_id)
489
-
523
+
490
524
  # Then update collections (UI item now exists to receive them)
491
525
  self.connection_manager.update_collections(connection_id, collections)
492
-
526
+
493
527
  # Switch to Active connections tab
494
528
  self.left_tabs.setCurrentIndex(0)
495
-
496
- self.statusBar().showMessage(f"Connected successfully ({len(collections)} collections)", 5000)
529
+
530
+ self.statusBar().showMessage(
531
+ f"Connected successfully ({len(collections)} collections)", 5000
532
+ )
497
533
  else:
498
534
  # Update state to error
499
- self.connection_manager.update_connection_state(connection_id, ConnectionState.ERROR, error)
500
-
535
+ self.connection_manager.update_connection_state(
536
+ connection_id, ConnectionState.ERROR, error
537
+ )
538
+
501
539
  QMessageBox.warning(self, "Connection Failed", f"Failed to connect: {error}")
502
-
540
+
503
541
  # Remove the failed connection
504
542
  self.connection_manager.close_connection(connection_id)
505
-
543
+
506
544
  def _refresh_active_connection(self):
507
545
  """Refresh collections for the active connection."""
508
546
  active = self.connection_manager.get_active_connection()
509
547
  if not active or not active.connection.is_connected:
510
548
  QMessageBox.information(self, "No Connection", "No active connection to refresh.")
511
549
  return
512
-
550
+
513
551
  try:
514
552
  collections = active.connection.list_collections()
515
553
  self.connection_manager.update_collections(active.id, collections)
516
554
  self.statusBar().showMessage(f"Refreshed collections ({len(collections)} found)", 3000)
517
-
555
+
518
556
  # Also refresh info panel
519
557
  self.info_panel.refresh_database_info()
520
558
  except Exception as e:
521
559
  QMessageBox.warning(self, "Refresh Failed", f"Failed to refresh collections: {e}")
522
-
560
+
523
561
  def _restore_session(self):
524
562
  """Restore previously active connections on startup."""
525
563
  # TODO: Implement session restore
@@ -528,29 +566,25 @@ class MainWindow(QMainWindow):
528
566
  if profiles:
529
567
  self.statusBar().showMessage(
530
568
  f"{len(profiles)} saved profile(s) available. Switch to Profiles tab to connect.",
531
- 10000
569
+ 10000,
532
570
  )
533
-
571
+
534
572
  def _show_about(self):
535
573
  """Show about dialog."""
574
+ from .main_window import get_about_html
575
+
536
576
  QMessageBox.about(
537
577
  self,
538
578
  "About Vector Inspector",
539
- "<h2>Vector Inspector 0.3.0</h2>"
540
- "<p>A comprehensive desktop application for visualizing, "
541
- "querying, and managing multiple vector databases simultaneously.</p>"
542
- '<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
543
- "<hr />"
544
- "<p>Built with PySide6, ChromaDB, and Qdrant</p>"
545
- "<p><b>New:</b> Multi-database support with saved connection profiles</p>"
579
+ get_about_html(),
546
580
  )
547
-
581
+
548
582
  def _toggle_cache(self, checked: bool):
549
583
  """Toggle caching on/off."""
550
584
  self.settings_service.set_cache_enabled(checked)
551
585
  status = "enabled" if checked else "disabled"
552
586
  self.statusBar().showMessage(f"Caching {status}", 3000)
553
-
587
+
554
588
  def _show_migration_dialog(self):
555
589
  """Show cross-database migration dialog."""
556
590
  if self.connection_manager.get_connection_count() < 2:
@@ -558,26 +592,23 @@ class MainWindow(QMainWindow):
558
592
  self,
559
593
  "Insufficient Connections",
560
594
  "You need at least 2 active connections to migrate data.\n"
561
- "Please connect to additional databases first."
595
+ "Please connect to additional databases first.",
562
596
  )
563
597
  return
564
-
598
+
565
599
  from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
600
+
566
601
  dialog = CrossDatabaseMigrationDialog(self.connection_manager, self)
567
602
  dialog.exec()
568
-
603
+
569
604
  def _show_backup_restore_dialog(self):
570
605
  """Show backup/restore dialog for the active collection."""
571
606
  # Check if there's an active connection
572
607
  connection = self.connection_manager.get_active_connection()
573
608
  if not connection:
574
- QMessageBox.information(
575
- self,
576
- "No Connection",
577
- "Please connect to a database first."
578
- )
609
+ QMessageBox.information(self, "No Connection", "Please connect to a database first.")
579
610
  return
580
-
611
+
581
612
  # Get active collection
582
613
  collection_name = self.connection_manager.get_active_collection()
583
614
  if not collection_name:
@@ -586,15 +617,16 @@ class MainWindow(QMainWindow):
586
617
  self,
587
618
  "No Collection Selected",
588
619
  "You can restore backups without a collection selected.\n"
589
- "To create a backup, please select a collection first."
620
+ "To create a backup, please select a collection first.",
590
621
  )
591
-
622
+
592
623
  from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
624
+
593
625
  dialog = BackupRestoreDialog(connection, collection_name or "", self)
594
626
  if dialog.exec() == QDialog.Accepted:
595
627
  # Refresh collections after restore
596
628
  self._refresh_active_connection()
597
-
629
+
598
630
  def closeEvent(self, event):
599
631
  """Handle application close."""
600
632
  # Wait for all connection threads to finish
@@ -602,9 +634,31 @@ class MainWindow(QMainWindow):
602
634
  if thread.isRunning():
603
635
  thread.quit()
604
636
  thread.wait(1000) # Wait up to 1 second
605
-
637
+
638
+ # Clean up temp HTML files from visualization view
639
+ if self.visualization_view is not None:
640
+ try:
641
+ self.visualization_view.cleanup_temp_html()
642
+ except Exception:
643
+ pass
606
644
  # Close all connections
607
645
  self.connection_manager.close_all_connections()
608
-
646
+
609
647
  event.accept()
610
648
 
649
+
650
+ def get_about_html():
651
+ from vector_inspector.utils.version import get_app_version
652
+
653
+ version = get_app_version()
654
+ version_html = (
655
+ f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
656
+ )
657
+ return (
658
+ version_html + "<p>A comprehensive desktop application for visualizing, "
659
+ "querying, and managing multiple vector databases simultaneously.</p>"
660
+ '<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
661
+ "<hr />"
662
+ "<p>Built with PySide6</p>"
663
+ "<p><b>New:</b> Pinecone support!</p>"
664
+ )