vector-inspector 0.2.7__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 (31) hide show
  1. vector_inspector/config/__init__.py +4 -0
  2. vector_inspector/config/known_embedding_models.json +432 -0
  3. vector_inspector/core/connections/__init__.py +2 -1
  4. vector_inspector/core/connections/base_connection.py +42 -1
  5. vector_inspector/core/connections/chroma_connection.py +47 -11
  6. vector_inspector/core/connections/pinecone_connection.py +768 -0
  7. vector_inspector/core/embedding_providers/__init__.py +14 -0
  8. vector_inspector/core/embedding_providers/base_provider.py +128 -0
  9. vector_inspector/core/embedding_providers/clip_provider.py +260 -0
  10. vector_inspector/core/embedding_providers/provider_factory.py +176 -0
  11. vector_inspector/core/embedding_providers/sentence_transformer_provider.py +203 -0
  12. vector_inspector/core/embedding_utils.py +69 -42
  13. vector_inspector/core/model_registry.py +205 -0
  14. vector_inspector/services/backup_restore_service.py +16 -0
  15. vector_inspector/services/settings_service.py +117 -1
  16. vector_inspector/ui/components/connection_manager_panel.py +7 -0
  17. vector_inspector/ui/components/profile_manager_panel.py +61 -14
  18. vector_inspector/ui/dialogs/__init__.py +2 -1
  19. vector_inspector/ui/dialogs/cross_db_migration.py +20 -1
  20. vector_inspector/ui/dialogs/embedding_config_dialog.py +166 -27
  21. vector_inspector/ui/dialogs/provider_type_dialog.py +189 -0
  22. vector_inspector/ui/main_window.py +33 -2
  23. vector_inspector/ui/views/connection_view.py +55 -10
  24. vector_inspector/ui/views/info_panel.py +83 -36
  25. vector_inspector/ui/views/search_view.py +1 -1
  26. vector_inspector/ui/views/visualization_view.py +19 -5
  27. {vector_inspector-0.2.7.dist-info → vector_inspector-0.3.1.dist-info}/METADATA +1 -1
  28. vector_inspector-0.3.1.dist-info/RECORD +55 -0
  29. vector_inspector-0.2.7.dist-info/RECORD +0 -45
  30. {vector_inspector-0.2.7.dist-info → vector_inspector-0.3.1.dist-info}/WHEEL +0 -0
  31. {vector_inspector-0.2.7.dist-info → vector_inspector-0.3.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,189 @@
1
+ """Dialog for selecting embedding provider type (Step 1 of model selection)."""
2
+
3
+ from typing import Optional, Tuple
4
+ from PySide6.QtWidgets import (
5
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel,
6
+ QPushButton, QRadioButton, QButtonGroup, QGroupBox,
7
+ QScrollArea, QWidget
8
+ )
9
+ from PySide6.QtCore import Qt
10
+
11
+ from vector_inspector.core.model_registry import get_model_registry
12
+
13
+
14
+ class ProviderTypeDialog(QDialog):
15
+ """Dialog for selecting the provider/type category before choosing specific model."""
16
+
17
+ # Provider categories with display info
18
+ PROVIDER_CATEGORIES = [
19
+ {
20
+ "id": "sentence-transformer",
21
+ "name": "🤗 Sentence Transformers",
22
+ "description": "Local models from HuggingFace\nNo API key required, runs on your machine",
23
+ "filter_type": "sentence-transformer",
24
+ "icon": "📦"
25
+ },
26
+ {
27
+ "id": "clip",
28
+ "name": "🖼️ CLIP Models",
29
+ "description": "Multimodal embeddings (text + images)\nLocal models, no API key required",
30
+ "filter_type": "clip",
31
+ "icon": "🎨"
32
+ },
33
+ {
34
+ "id": "openai",
35
+ "name": "☁️ OpenAI API",
36
+ "description": "Cloud-based embeddings\nRequires OpenAI API key",
37
+ "filter_type": "openai",
38
+ "icon": "🔑"
39
+ },
40
+ {
41
+ "id": "cohere",
42
+ "name": "☁️ Cohere API",
43
+ "description": "Cloud-based embeddings\nRequires Cohere API key",
44
+ "filter_type": "cohere",
45
+ "icon": "🔑"
46
+ },
47
+ {
48
+ "id": "vertex-ai",
49
+ "name": "☁️ Google Vertex AI",
50
+ "description": "Cloud-based embeddings\nRequires Google Cloud credentials",
51
+ "filter_type": "vertex-ai",
52
+ "icon": "🔑"
53
+ },
54
+ {
55
+ "id": "voyage",
56
+ "name": "☁️ Voyage AI",
57
+ "description": "Cloud-based embeddings\nRequires Voyage API key",
58
+ "filter_type": "voyage",
59
+ "icon": "🔑"
60
+ },
61
+ {
62
+ "id": "custom",
63
+ "name": "✏️ Custom Model",
64
+ "description": "Enter your own model name\nFor models not in the registry",
65
+ "filter_type": None, # Special case
66
+ "icon": "⚙️"
67
+ }
68
+ ]
69
+
70
+ def __init__(self, collection_name: str, vector_dimension: int, parent=None):
71
+ super().__init__(parent)
72
+ self.collection_name = collection_name
73
+ self.vector_dimension = vector_dimension
74
+ self.selected_type = None
75
+
76
+ self.setWindowTitle("Select Embedding Provider Type")
77
+ self.setMinimumWidth(550)
78
+ self.setMinimumHeight(500)
79
+ self._setup_ui()
80
+
81
+ def _setup_ui(self):
82
+ """Setup dialog UI."""
83
+ layout = QVBoxLayout(self)
84
+
85
+ # Header
86
+ header = QLabel(f"<h3>Select Provider Type</h3>")
87
+ layout.addWidget(header)
88
+
89
+ info = QLabel(f"<b>Collection:</b> {self.collection_name}<br>"
90
+ f"<b>Vector Dimension:</b> {self.vector_dimension}")
91
+ layout.addWidget(info)
92
+
93
+ layout.addWidget(QLabel("<i>Choose the type of embedding provider to use:</i>"))
94
+
95
+ # Scroll area for provider options
96
+ scroll = QScrollArea()
97
+ scroll.setWidgetResizable(True)
98
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
99
+
100
+ scroll_widget = QWidget()
101
+ scroll_layout = QVBoxLayout(scroll_widget)
102
+ scroll_layout.setSpacing(10)
103
+
104
+ # Radio button group
105
+ self.button_group = QButtonGroup(self)
106
+
107
+ # Get registry to count models
108
+ registry = get_model_registry()
109
+
110
+ # Create radio buttons for each provider type
111
+ for i, category in enumerate(self.PROVIDER_CATEGORIES):
112
+ provider_id = category["id"]
113
+
114
+ # Count available models for this type + dimension
115
+ if provider_id == "custom":
116
+ count_text = "Enter manually"
117
+ else:
118
+ filter_type = category["filter_type"]
119
+ models = registry.get_models_by_dimension(self.vector_dimension)
120
+ matching = [m for m in models if m.type == filter_type]
121
+ count = len(matching)
122
+
123
+ if count == 0:
124
+ continue # Skip categories with no models for this dimension
125
+
126
+ count_text = f"{count} model{'s' if count != 1 else ''} available"
127
+
128
+ # Create group box for this option
129
+ group = QGroupBox()
130
+ group_layout = QVBoxLayout()
131
+
132
+ # Radio button
133
+ radio = QRadioButton(category["name"])
134
+ radio.setProperty("provider_id", provider_id)
135
+ self.button_group.addButton(radio, i)
136
+ group_layout.addWidget(radio)
137
+
138
+ # Description
139
+ desc_label = QLabel(category["description"])
140
+ desc_label.setStyleSheet("color: gray; margin-left: 25px;")
141
+ desc_label.setWordWrap(True)
142
+ group_layout.addWidget(desc_label)
143
+
144
+ # Count
145
+ count_label = QLabel(f"<b>{count_text}</b>")
146
+ count_label.setStyleSheet("margin-left: 25px; color: #0066cc;")
147
+ group_layout.addWidget(count_label)
148
+
149
+ group.setLayout(group_layout)
150
+ scroll_layout.addWidget(group)
151
+
152
+ scroll_layout.addStretch()
153
+ scroll.setWidget(scroll_widget)
154
+ layout.addWidget(scroll)
155
+
156
+ # Buttons
157
+ button_layout = QHBoxLayout()
158
+ button_layout.addStretch()
159
+
160
+ cancel_btn = QPushButton("Cancel")
161
+ cancel_btn.clicked.connect(self.reject)
162
+
163
+ self.next_btn = QPushButton("Next →")
164
+ self.next_btn.clicked.connect(self._on_next)
165
+ self.next_btn.setEnabled(False)
166
+ self.next_btn.setDefault(True)
167
+
168
+ # Enable Next when selection is made
169
+ self.button_group.buttonClicked.connect(lambda: self.next_btn.setEnabled(True))
170
+
171
+ button_layout.addWidget(cancel_btn)
172
+ button_layout.addWidget(self.next_btn)
173
+
174
+ layout.addLayout(button_layout)
175
+
176
+ def _on_next(self):
177
+ """Handle Next button click."""
178
+ selected_button = self.button_group.checkedButton()
179
+ if selected_button:
180
+ self.selected_type = selected_button.property("provider_id")
181
+ self.accept()
182
+
183
+ def get_selected_type(self) -> Optional[str]:
184
+ """Get the selected provider type ID.
185
+
186
+ Returns:
187
+ Provider type ID or None if cancelled
188
+ """
189
+ return self.selected_type
@@ -12,6 +12,7 @@ from vector_inspector.core.connection_manager import ConnectionManager, Connecti
12
12
  from vector_inspector.core.connections.base_connection import VectorDBConnection
13
13
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
14
14
  from vector_inspector.core.connections.qdrant_connection import QdrantConnection
15
+ from vector_inspector.core.connections.pinecone_connection import PineconeConnection
15
16
  from vector_inspector.services.profile_service import ProfileService
16
17
  from vector_inspector.services.settings_service import SettingsService
17
18
  from vector_inspector.ui.components.connection_manager_panel import ConnectionManagerPanel
@@ -219,6 +220,7 @@ class MainWindow(QMainWindow):
219
220
  self.connection_manager.active_connection_changed.connect(self._on_active_connection_changed)
220
221
  self.connection_manager.active_collection_changed.connect(self._on_active_collection_changed)
221
222
  self.connection_manager.collections_updated.connect(self._on_collections_updated)
223
+ self.connection_manager.connection_opened.connect(self._on_connection_opened)
222
224
 
223
225
  # Connection panel signals
224
226
  self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
@@ -277,17 +279,36 @@ class MainWindow(QMainWindow):
277
279
 
278
280
  # Update views if this is the active connection
279
281
  if connection_id == self.connection_manager.get_active_connection_id():
282
+ # Show loading immediately when collection changes
280
283
  if collection_name:
281
- self._update_views_for_collection(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()
282
290
  else:
283
291
  # Clear collection from views
284
- self._update_views_for_collection(None)
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()
285
298
 
286
299
  def _on_collections_updated(self, connection_id: str, collections: list):
287
300
  """Handle collections list updated."""
288
301
  # UI automatically updates via connection_manager_panel
289
302
  pass
290
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
+
291
312
  def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
292
313
  """Handle collection selection from connection panel."""
293
314
  # Show loading dialog while switching collections
@@ -378,6 +399,8 @@ class MainWindow(QMainWindow):
378
399
  connection = self._create_chroma_connection(config, credentials)
379
400
  elif provider == "qdrant":
380
401
  connection = self._create_qdrant_connection(config, credentials)
402
+ elif provider == "pinecone":
403
+ connection = self._create_pinecone_connection(config, credentials)
381
404
  else:
382
405
  QMessageBox.warning(self, "Error", f"Unsupported provider: {provider}")
383
406
  return
@@ -439,6 +462,14 @@ class MainWindow(QMainWindow):
439
462
  else: # ephemeral
440
463
  return QdrantConnection()
441
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
+
442
473
  def _on_connection_finished(self, connection_id: str, success: bool, collections: list, error: str):
443
474
  """Handle connection thread completion."""
444
475
  self.loading_dialog.hide_loading()
@@ -10,6 +10,7 @@ from PySide6.QtCore import Signal, QThread
10
10
  from vector_inspector.core.connections.base_connection import VectorDBConnection
11
11
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
12
12
  from vector_inspector.core.connections.qdrant_connection import QdrantConnection
13
+ from vector_inspector.core.connections.pinecone_connection import PineconeConnection
13
14
  from vector_inspector.ui.components.loading_dialog import LoadingDialog
14
15
  from vector_inspector.services.settings_service import SettingsService
15
16
 
@@ -66,6 +67,7 @@ class ConnectionDialog(QDialog):
66
67
  self.provider_combo = QComboBox()
67
68
  self.provider_combo.addItem("ChromaDB", "chromadb")
68
69
  self.provider_combo.addItem("Qdrant", "qdrant")
70
+ self.provider_combo.addItem("Pinecone", "pinecone")
69
71
  self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
70
72
  provider_layout.addWidget(self.provider_combo)
71
73
  provider_group.setLayout(provider_layout)
@@ -179,19 +181,42 @@ class ConnectionDialog(QDialog):
179
181
  if self.port_input.text() == "6333":
180
182
  self.port_input.setText("8000")
181
183
 
182
- # Show/hide API key field
183
- is_http = self.http_radio.isChecked()
184
- self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
184
+ # For Pinecone, hide persistent/HTTP options and only show API key
185
+ if self.provider == "pinecone":
186
+ self.persistent_radio.setEnabled(False)
187
+ self.http_radio.setEnabled(True)
188
+ self.http_radio.setChecked(True)
189
+ self.ephemeral_radio.setEnabled(False)
190
+ self.path_input.setEnabled(False)
191
+ self.host_input.setEnabled(False)
192
+ self.port_input.setEnabled(False)
193
+ self.api_key_input.setEnabled(True)
194
+ else:
195
+ self.persistent_radio.setEnabled(True)
196
+ self.http_radio.setEnabled(True)
197
+ self.ephemeral_radio.setEnabled(True)
198
+ # Show/hide API key field
199
+ is_http = self.http_radio.isChecked()
200
+ self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
201
+ # Update path/host/port based on connection type
202
+ self._on_type_changed()
185
203
 
186
204
  def _on_type_changed(self):
187
205
  """Handle connection type change."""
188
206
  is_persistent = self.persistent_radio.isChecked()
189
207
  is_http = self.http_radio.isChecked()
190
208
 
191
- self.path_input.setEnabled(is_persistent)
192
- self.host_input.setEnabled(is_http)
193
- self.port_input.setEnabled(is_http)
194
- self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
209
+ # Pinecone always uses API key, no path/host/port
210
+ if self.provider == "pinecone":
211
+ self.path_input.setEnabled(False)
212
+ self.host_input.setEnabled(False)
213
+ self.port_input.setEnabled(False)
214
+ self.api_key_input.setEnabled(True)
215
+ else:
216
+ self.path_input.setEnabled(is_persistent)
217
+ self.host_input.setEnabled(is_http)
218
+ self.port_input.setEnabled(is_http)
219
+ self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
195
220
 
196
221
  self._update_absolute_preview()
197
222
 
@@ -202,7 +227,13 @@ class ConnectionDialog(QDialog):
202
227
 
203
228
  config = {"provider": self.provider}
204
229
 
205
- if self.persistent_radio.isChecked():
230
+ # Pinecone only needs API key
231
+ if self.provider == "pinecone":
232
+ config.update({
233
+ "type": "cloud",
234
+ "api_key": self.api_key_input.text()
235
+ })
236
+ elif self.persistent_radio.isChecked():
206
237
  config.update({"type": "persistent", "path": self.path_input.text()})
207
238
  elif self.http_radio.isChecked():
208
239
  config.update({
@@ -286,7 +317,13 @@ class ConnectionDialog(QDialog):
286
317
 
287
318
  # Set connection type
288
319
  conn_type = last_config.get("type", "persistent")
289
- if conn_type == "persistent":
320
+ if conn_type == "cloud":
321
+ # Pinecone cloud connection
322
+ self.http_radio.setChecked(True)
323
+ api_key = last_config.get("api_key")
324
+ if api_key:
325
+ self.api_key_input.setText(api_key)
326
+ elif conn_type == "persistent":
290
327
  self.persistent_radio.setChecked(True)
291
328
  path = last_config.get("path", "")
292
329
  if path:
@@ -370,7 +407,15 @@ class ConnectionView(QWidget):
370
407
  conn_type = config.get("type")
371
408
 
372
409
  # Create appropriate connection instance based on provider
373
- if provider == "qdrant":
410
+ if provider == "pinecone":
411
+ api_key = config.get("api_key")
412
+ if not api_key:
413
+ self.loading_dialog.hide_loading()
414
+ from PySide6.QtWidgets import QMessageBox
415
+ QMessageBox.warning(self, "Missing API Key", "Pinecone requires an API key to connect.")
416
+ return
417
+ self.connection = PineconeConnection(api_key=api_key)
418
+ elif provider == "qdrant":
374
419
  if conn_type == "persistent":
375
420
  self.connection = QdrantConnection(path=config.get("path"))
376
421
  elif conn_type == "http":
@@ -7,10 +7,12 @@ from PySide6.QtWidgets import (
7
7
  )
8
8
  from PySide6.QtCore import Qt, QObject
9
9
  from PySide6.QtWidgets import QDialog
10
+ from PySide6.QtWidgets import QApplication
10
11
 
11
12
  from vector_inspector.core.connections.base_connection import VectorDBConnection
12
13
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
13
14
  from vector_inspector.core.connections.qdrant_connection import QdrantConnection
15
+ from vector_inspector.core.connections.pinecone_connection import PineconeConnection
14
16
  from vector_inspector.core.cache_manager import get_cache_manager
15
17
 
16
18
 
@@ -199,6 +201,15 @@ class InfoPanel(QWidget):
199
201
  self._update_label(self.api_key_label, "Present (hidden)")
200
202
  else:
201
203
  self._update_label(self.api_key_label, "Not configured")
204
+
205
+ elif isinstance(self.connection, PineconeConnection):
206
+ self._update_label(self.connection_type_label, "Cloud")
207
+ self._update_label(self.endpoint_label, "Pinecone Cloud")
208
+
209
+ if self.connection.api_key:
210
+ self._update_label(self.api_key_label, "Present (hidden)")
211
+ else:
212
+ self._update_label(self.api_key_label, "Not configured")
202
213
  else:
203
214
  self._update_label(self.connection_type_label, "Unknown")
204
215
  self._update_label(self.endpoint_label, "N/A")
@@ -317,6 +328,18 @@ class InfoPanel(QWidget):
317
328
  opt = config["optimizer_config"]
318
329
  details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
319
330
 
331
+ elif isinstance(self.connection, PineconeConnection):
332
+ details_list.append("• Provider: Pinecone")
333
+ details_list.append("• Supports: Vectors, Metadata")
334
+ details_list.append("• Cloud-hosted vector database")
335
+ # Add Pinecone-specific info if available
336
+ if "host" in collection_info:
337
+ details_list.append(f"• Host: {collection_info['host']}")
338
+ if "status" in collection_info:
339
+ details_list.append(f"• Status: {collection_info['status']}")
340
+ if "spec" in collection_info:
341
+ details_list.append(f"• Spec: {collection_info['spec']}")
342
+
320
343
  if details_list:
321
344
  self.provider_details_label.setText("\n".join(details_list))
322
345
  self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
@@ -358,18 +381,24 @@ class InfoPanel(QWidget):
358
381
  # Check if stored in collection metadata
359
382
  if 'embedding_model' in collection_info:
360
383
  model_name = collection_info['embedding_model']
361
- model_type = collection_info.get('embedding_model_type', 'unknown')
384
+ model_type = collection_info.get('embedding_model_type', 'stored')
362
385
  self.embedding_model_label.setText(f"{model_name} ({model_type})")
363
386
  self.embedding_model_label.setStyleSheet("color: lightgreen;")
364
387
  return
365
388
 
389
+ # Try to get from connection using the helper method
390
+ if self.connection and self.current_collection:
391
+ detected_model = self.connection.get_embedding_model(self.current_collection, self.connection_id)
392
+ if detected_model:
393
+ self.embedding_model_label.setText(f"{detected_model} (detected)")
394
+ self.embedding_model_label.setStyleSheet("color: lightgreen;")
395
+ return
396
+
366
397
  # Check user settings
367
398
  settings = SettingsService()
368
- collection_models = settings.get('collection_embedding_models', {})
369
- collection_key = f"{self.connection_id}:{self.current_collection}"
399
+ model_info = settings.get_embedding_model(self.connection_id, self.current_collection)
370
400
 
371
- if collection_key in collection_models:
372
- model_info = collection_models[collection_key]
401
+ if model_info:
373
402
  model_name = model_info['model']
374
403
  model_type = model_info.get('type', 'unknown')
375
404
  self.embedding_model_label.setText(f"{model_name} ({model_type})")
@@ -385,11 +414,21 @@ class InfoPanel(QWidget):
385
414
  if not self.current_collection:
386
415
  return
387
416
 
388
- from ..dialogs.embedding_config_dialog import EmbeddingConfigDialog
417
+ # Show loading immediately; preparing can touch DB/registry
418
+ from ..components.loading_dialog import LoadingDialog
419
+ loading = LoadingDialog("Preparing model configuration...", self)
420
+ loading.show_loading("Preparing model configuration...")
421
+ QApplication.processEvents()
422
+
423
+ from ..dialogs import ProviderTypeDialog, EmbeddingConfigDialog
389
424
  from ...services.settings_service import SettingsService
390
425
 
391
426
  # Get current collection info
392
- collection_info = self.connection.get_collection_info(self.current_collection)
427
+ try:
428
+ collection_info = self.connection.get_collection_info(self.current_collection)
429
+ finally:
430
+ # Hide loading before presenting dialogs
431
+ loading.hide_loading()
393
432
  if not collection_info:
394
433
  return
395
434
 
@@ -399,8 +438,6 @@ class InfoPanel(QWidget):
399
438
 
400
439
  # Get current configuration if any
401
440
  settings = SettingsService()
402
- collection_models = settings.get('collection_embedding_models', {})
403
- collection_key = f"{self.connection_id}:{self.current_collection}"
404
441
 
405
442
  current_model = None
406
443
  current_type = None
@@ -408,37 +445,49 @@ class InfoPanel(QWidget):
408
445
  # Check metadata first
409
446
  if 'embedding_model' in collection_info:
410
447
  current_model = collection_info['embedding_model']
411
- current_type = collection_info.get('embedding_model_type')
448
+ current_type = collection_info.get('embedding_model_type', 'stored')
412
449
  # Then check settings
413
- elif collection_key in collection_models:
414
- model_info = collection_models[collection_key]
415
- current_model = model_info.get('model')
416
- current_type = model_info.get('type')
450
+ else:
451
+ model_info = settings.get_embedding_model(self.connection_id, self.current_collection)
452
+ if model_info:
453
+ current_model = model_info.get('model')
454
+ current_type = model_info.get('type')
455
+
456
+ # Step 1: Provider Type Selection
457
+ type_dialog = ProviderTypeDialog(
458
+ self.current_collection,
459
+ vector_dim,
460
+ self
461
+ )
462
+
463
+ type_result = type_dialog.exec()
464
+ if type_result != QDialog.DialogCode.Accepted:
465
+ return # User cancelled
417
466
 
418
- # Open dialog
419
- dialog = EmbeddingConfigDialog(
467
+ provider_type = type_dialog.get_selected_type()
468
+ if not provider_type:
469
+ return
470
+
471
+ # Step 2: Model Selection (filtered by provider type)
472
+ model_dialog = EmbeddingConfigDialog(
420
473
  self.current_collection,
421
474
  vector_dim,
475
+ provider_type,
422
476
  current_model,
423
477
  current_type,
424
478
  self
425
479
  )
426
480
 
427
- result = dialog.exec()
481
+ # Optionally show brief loading while populating models
482
+ # (dialog itself handles content; only show if provider lists are large)
483
+ result = model_dialog.exec()
428
484
 
429
485
  if result == QDialog.DialogCode.Accepted:
430
- # Save the configuration
431
- selection = dialog.get_selection()
486
+ # Save the configuration using the new SettingsService method
487
+ selection = model_dialog.get_selection()
432
488
  if selection:
433
489
  model_name, model_type = selection
434
-
435
- if collection_key not in collection_models:
436
- collection_models[collection_key] = {}
437
-
438
- collection_models[collection_key]['model'] = model_name
439
- collection_models[collection_key]['type'] = model_type
440
-
441
- settings.set('collection_embedding_models', collection_models)
490
+ settings.save_embedding_model(self.connection_id, self.current_collection, model_name, model_type)
442
491
 
443
492
  # Refresh display
444
493
  self._update_embedding_model_display(collection_info)
@@ -446,12 +495,10 @@ class InfoPanel(QWidget):
446
495
  print(f"✓ Configured embedding model for '{self.current_collection}': {model_name} ({model_type})")
447
496
 
448
497
  elif result == 2: # Clear configuration
449
- # Remove from settings
450
- if collection_key in collection_models:
451
- del collection_models[collection_key]
452
- settings.set('collection_embedding_models', collection_models)
453
-
454
- # Refresh display
455
- self._update_embedding_model_display(collection_info)
456
-
457
- print(f"✓ Cleared embedding model configuration for '{self.current_collection}'")
498
+ # Remove from settings using the new SettingsService method
499
+ settings.remove_embedding_model(self.connection_id, self.current_collection)
500
+
501
+ # Refresh display
502
+ self._update_embedding_model_display(collection_info)
503
+
504
+ print(f"✓ Cleared embedding model configuration for '{self.current_collection}'")
@@ -202,7 +202,7 @@ class SearchView(QWidget):
202
202
  QApplication.processEvents()
203
203
 
204
204
  try:
205
- # Perform query
205
+ # Always pass query_texts; provider handles embedding if needed
206
206
  results = self.connection.query_collection(
207
207
  self.current_collection,
208
208
  query_texts=[query_text],
@@ -13,6 +13,7 @@ import numpy as np
13
13
 
14
14
  from vector_inspector.core.connections.base_connection import VectorDBConnection
15
15
  from vector_inspector.services.visualization_service import VisualizationService
16
+ from vector_inspector.ui.components.loading_dialog import LoadingDialog
16
17
 
17
18
 
18
19
  class VisualizationThread(QThread):
@@ -106,6 +107,9 @@ class VisualizationView(QWidget):
106
107
  self.status_label.setMaximumHeight(30)
107
108
  layout.addWidget(self.status_label)
108
109
 
110
+ # Loading dialog for data fetch and reduction
111
+ self.loading_dialog = LoadingDialog("Loading visualization...", self)
112
+
109
113
  def set_collection(self, collection_name: str):
110
114
  """Set the current collection to visualize."""
111
115
  self.current_collection = collection_name
@@ -119,12 +123,17 @@ class VisualizationView(QWidget):
119
123
  QMessageBox.warning(self, "No Collection", "Please select a collection first.")
120
124
  return
121
125
 
122
- # Load data with embeddings
126
+ # Load data with embeddings (show loading immediately)
127
+ self.loading_dialog.show_loading("Loading data for visualization...")
128
+ QApplication.processEvents()
123
129
  sample_size = self.sample_spin.value()
124
- data = self.connection.get_all_items(
125
- self.current_collection,
126
- limit=sample_size
127
- )
130
+ try:
131
+ data = self.connection.get_all_items(
132
+ self.current_collection,
133
+ limit=sample_size
134
+ )
135
+ finally:
136
+ self.loading_dialog.hide_loading()
128
137
 
129
138
  if data is None or not data or "embeddings" not in data or data["embeddings"] is None or len(data["embeddings"]) == 0:
130
139
  QMessageBox.warning(
@@ -152,10 +161,14 @@ class VisualizationView(QWidget):
152
161
  )
153
162
  self.visualization_thread.finished.connect(self._on_reduction_finished)
154
163
  self.visualization_thread.error.connect(self._on_reduction_error)
164
+ # Show loading during reduction
165
+ self.loading_dialog.show_loading("Reducing dimensions...")
166
+ QApplication.processEvents()
155
167
  self.visualization_thread.start()
156
168
 
157
169
  def _on_reduction_finished(self, reduced_data: Any):
158
170
  """Handle dimensionality reduction completion."""
171
+ self.loading_dialog.hide_loading()
159
172
  self.reduced_data = reduced_data
160
173
  self._create_plot()
161
174
  self.generate_button.setEnabled(True)
@@ -163,6 +176,7 @@ class VisualizationView(QWidget):
163
176
 
164
177
  def _on_reduction_error(self, error_msg: str):
165
178
  """Handle dimensionality reduction error."""
179
+ self.loading_dialog.hide_loading()
166
180
  print(f"Error: Visualization failed: {error_msg}")
167
181
  QMessageBox.warning(self, "Error", f"Visualization failed: {error_msg}")
168
182
  self.generate_button.setEnabled(True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vector-inspector
3
- Version: 0.2.7
3
+ Version: 0.3.1
4
4
  Summary: A comprehensive desktop application for visualizing, querying, and managing vector database data
5
5
  Author-Email: Anthony Dawson <anthonypdawson+github@gmail.com>
6
6
  License: MIT