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