vector-inspector 0.3.9__py3-none-any.whl → 0.3.12__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 (33) hide show
  1. vector_inspector/__init__.py +10 -1
  2. vector_inspector/core/connection_manager.py +91 -19
  3. vector_inspector/core/connections/base_connection.py +43 -43
  4. vector_inspector/core/connections/chroma_connection.py +1 -1
  5. vector_inspector/core/connections/pgvector_connection.py +12 -172
  6. vector_inspector/core/connections/pinecone_connection.py +596 -99
  7. vector_inspector/core/connections/qdrant_connection.py +35 -44
  8. vector_inspector/core/embedding_utils.py +14 -5
  9. vector_inspector/core/logging.py +3 -1
  10. vector_inspector/extensions/__init__.py +6 -0
  11. vector_inspector/extensions/telemetry_settings_panel.py +25 -0
  12. vector_inspector/main.py +45 -2
  13. vector_inspector/services/backup_restore_service.py +228 -15
  14. vector_inspector/services/settings_service.py +79 -19
  15. vector_inspector/services/telemetry_service.py +88 -0
  16. vector_inspector/ui/components/backup_restore_dialog.py +215 -101
  17. vector_inspector/ui/components/connection_manager_panel.py +155 -14
  18. vector_inspector/ui/dialogs/cross_db_migration.py +126 -99
  19. vector_inspector/ui/dialogs/settings_dialog.py +13 -6
  20. vector_inspector/ui/loading_screen.py +169 -0
  21. vector_inspector/ui/main_window.py +44 -19
  22. vector_inspector/ui/services/dialog_service.py +1 -0
  23. vector_inspector/ui/views/collection_browser.py +36 -34
  24. vector_inspector/ui/views/connection_view.py +7 -1
  25. vector_inspector/ui/views/info_panel.py +118 -52
  26. vector_inspector/ui/views/metadata_view.py +30 -31
  27. vector_inspector/ui/views/search_view.py +20 -19
  28. vector_inspector/ui/views/visualization_view.py +18 -15
  29. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/METADATA +19 -37
  30. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/RECORD +33 -29
  31. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/WHEEL +1 -1
  32. vector_inspector-0.3.12.dist-info/licenses/LICENSE +1 -0
  33. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,169 @@
1
+ import os
2
+
3
+ from PySide6.QtCore import QPropertyAnimation, Qt
4
+ from PySide6.QtGui import QFont, QPixmap
5
+ from PySide6.QtWidgets import QCheckBox, QLabel, QVBoxLayout, QWidget
6
+
7
+
8
+ class LoadingScreen(QWidget):
9
+ def __init__(self, logo_path, version, app_name, tagline, loading_text):
10
+ super().__init__()
11
+ self.setWindowFlag(Qt.FramelessWindowHint)
12
+ self.setWindowFlag(Qt.WindowStaysOnTopHint)
13
+ self.setAttribute(Qt.WA_TranslucentBackground)
14
+
15
+ # Main layout
16
+ layout = QVBoxLayout(self)
17
+ layout.setContentsMargins(32, 32, 32, 32)
18
+ layout.setSpacing(12)
19
+
20
+ # Container with background
21
+ container = QWidget()
22
+ container.setStyleSheet("background-color: #222; border-radius: 10px; color: #fff;")
23
+ container_layout = QVBoxLayout(container)
24
+ container_layout.setContentsMargins(32, 32, 32, 32)
25
+ container_layout.setSpacing(12)
26
+
27
+ # Logo
28
+ if os.path.exists(logo_path):
29
+ pixmap = QPixmap(logo_path).scaled(
30
+ 128, 128, Qt.KeepAspectRatio, Qt.SmoothTransformation
31
+ )
32
+ logo_label = QLabel()
33
+ logo_label.setPixmap(pixmap)
34
+ logo_label.setAlignment(Qt.AlignCenter)
35
+ container_layout.addWidget(logo_label)
36
+
37
+ # App name
38
+ app_name_label = QLabel(app_name)
39
+ app_name_label.setFont(QFont("Segoe UI", 16, QFont.Bold))
40
+ app_name_label.setAlignment(Qt.AlignCenter)
41
+ app_name_label.setStyleSheet("color: #fff;")
42
+ container_layout.addWidget(app_name_label)
43
+
44
+ # Tagline
45
+ tagline_label = QLabel(tagline)
46
+ tagline_label.setFont(QFont("Segoe UI", 10))
47
+ tagline_label.setAlignment(Qt.AlignCenter)
48
+ tagline_label.setStyleSheet("color: #aaa;")
49
+ container_layout.addWidget(tagline_label)
50
+
51
+ # Version
52
+ version_label = QLabel(version)
53
+ version_label.setFont(QFont("Segoe UI", 10))
54
+ version_label.setAlignment(Qt.AlignCenter)
55
+ version_label.setStyleSheet("color: #aaa;")
56
+ container_layout.addWidget(version_label)
57
+
58
+ # Loading indicator
59
+ self.loading_label = QLabel(loading_text)
60
+ self.loading_label.setFont(QFont("Segoe UI", 10))
61
+ self.loading_label.setAlignment(Qt.AlignCenter)
62
+ self.loading_label.setStyleSheet("color: #6cf;")
63
+ container_layout.addWidget(self.loading_label)
64
+
65
+ # Skip loading screen checkbox
66
+ self.skip_loading_checkbox = QCheckBox("Don't show this again")
67
+ self.skip_loading_checkbox.setFont(QFont("Segoe UI", 9))
68
+ self.skip_loading_checkbox.setStyleSheet("color: #aaa; margin-top: 10px;")
69
+ self.skip_loading_checkbox.stateChanged.connect(self._on_skip_changed)
70
+ container_layout.addWidget(self.skip_loading_checkbox, alignment=Qt.AlignCenter)
71
+
72
+ layout.addWidget(container)
73
+ self.setLayout(layout)
74
+ self.resize(400, 400)
75
+
76
+ # Center on screen
77
+ self._center_on_screen()
78
+
79
+ def _center_on_screen(self):
80
+ """Center the loading screen on the primary screen."""
81
+ from PySide6.QtWidgets import QApplication
82
+
83
+ screen = QApplication.primaryScreen().geometry()
84
+ size = self.geometry()
85
+ self.move((screen.width() - size.width()) // 2, (screen.height() - size.height()) // 2)
86
+
87
+ def set_loading_text(self, text):
88
+ self.loading_label.setText(text)
89
+
90
+ def _on_skip_changed(self, state):
91
+ """Save the skip loading screen preference."""
92
+ from vector_inspector.services.settings_service import SettingsService
93
+
94
+ settings = SettingsService()
95
+ settings.set("hide_loading_screen", state == Qt.CheckState.Checked.value)
96
+
97
+ def fade_out(self, duration=500):
98
+ """Fade out the loading screen and close after animation."""
99
+ animation = QPropertyAnimation(self, b"windowOpacity")
100
+ animation.setDuration(duration)
101
+ animation.setStartValue(1.0)
102
+ animation.setEndValue(0.0)
103
+ animation.finished.connect(self.close)
104
+ animation.start()
105
+ self._fade_animation = animation # Prevent garbage collection
106
+
107
+
108
+ def show_loading_screen(app_name, version, tagline, loading_text="Initializing providers…", logo_path=None):
109
+ """Show the loading screen if not disabled in settings.
110
+
111
+ This is a convenience function that handles checking settings, finding the logo,
112
+ creating the loading screen, and showing it. Both vector-inspector and vector-studio
113
+ should use this function to avoid code duplication.
114
+
115
+ Args:
116
+ app_name: Name of the application (e.g., "Vector Inspector", "Vector Studio")
117
+ version: Version string (e.g., "v0.3.11")
118
+ tagline: Tagline to display under app name
119
+ loading_text: Initial loading message
120
+ logo_path: Optional explicit path to logo. If None, will look for vector-inspector's logo.
121
+
122
+ Returns:
123
+ LoadingScreen instance if shown, None otherwise.
124
+ """
125
+ from vector_inspector.services.settings_service import SettingsService
126
+
127
+ # Check if user wants to skip loading screen
128
+ settings = SettingsService()
129
+ show_loading = not settings.get("hide_loading_screen", False)
130
+
131
+ if not show_loading:
132
+ return None
133
+
134
+ # Find logo path if not provided
135
+ if logo_path is None:
136
+ # Default to vector-inspector's logo
137
+ module_dir = os.path.dirname(os.path.abspath(__file__))
138
+ # Navigate from ui/loading_screen.py to vector_inspector/assets/logo.png
139
+ logo_path = os.path.join(os.path.dirname(module_dir), "assets", "logo.png")
140
+
141
+ # Create and show loading screen
142
+ loading = LoadingScreen(
143
+ logo_path=logo_path,
144
+ version=version,
145
+ app_name=app_name,
146
+ tagline=tagline,
147
+ loading_text=loading_text,
148
+ )
149
+ loading.show()
150
+
151
+ # Force the loading screen to render
152
+ from PySide6.QtWidgets import QApplication
153
+ QApplication.instance().processEvents()
154
+
155
+ return loading
156
+
157
+
158
+ # Example usage (to be called from main.py):
159
+ # from vector_inspector.ui.loading_screen import show_loading_screen
160
+ #
161
+ # loading = show_loading_screen(
162
+ # app_name="Vector Inspector",
163
+ # version="v0.3.11",
164
+ # tagline="The missing toolset for your vector data"
165
+ # )
166
+ # if loading:
167
+ # loading.set_loading_text("Loading main window...")
168
+ # # ... do initialization ...
169
+ # loading.fade_out()
@@ -1,26 +1,28 @@
1
1
  """Updated main window with multi-database support."""
2
2
 
3
+ from typing import Optional
4
+
5
+ from PySide6.QtCore import QByteArray, Qt, QTimer
6
+ from PySide6.QtGui import QAction
3
7
  from PySide6.QtWidgets import (
4
- QMessageBox,
5
- QLabel,
6
8
  QApplication,
7
9
  QDialog,
8
- QToolBar,
10
+ QLabel,
11
+ QMessageBox,
9
12
  QStatusBar,
13
+ QToolBar,
10
14
  )
11
- from PySide6.QtCore import Qt, QTimer, QByteArray
12
- from PySide6.QtGui import QAction
13
15
 
14
- from vector_inspector.core.connection_manager import ConnectionManager
15
- from vector_inspector.core.connections.base_connection import VectorDBConnection
16
+ from vector_inspector.core.connection_manager import ConnectionInstance, ConnectionManager
17
+ from vector_inspector.core.logging import log_error
16
18
  from vector_inspector.services.profile_service import ProfileService
17
19
  from vector_inspector.services.settings_service import SettingsService
18
- from vector_inspector.ui.main_window_shell import InspectorShell
19
20
  from vector_inspector.ui.components.connection_manager_panel import ConnectionManagerPanel
20
21
  from vector_inspector.ui.components.profile_manager_panel import ProfileManagerPanel
21
- from vector_inspector.ui.tabs import InspectorTabs
22
22
  from vector_inspector.ui.controllers.connection_controller import ConnectionController
23
+ from vector_inspector.ui.main_window_shell import InspectorShell
23
24
  from vector_inspector.ui.services.dialog_service import DialogService
25
+ from vector_inspector.ui.tabs import InspectorTabs
24
26
 
25
27
 
26
28
  class MainWindow(InspectorShell):
@@ -199,9 +201,10 @@ class MainWindow(InspectorShell):
199
201
  help_menu.addAction(check_update_action)
200
202
 
201
203
  def _check_for_update_from_menu(self):
204
+ from PySide6.QtWidgets import QMessageBox
205
+
202
206
  from vector_inspector.services.update_service import UpdateService
203
207
  from vector_inspector.utils.version import get_app_version
204
- from PySide6.QtWidgets import QMessageBox
205
208
 
206
209
  latest = UpdateService.get_latest_release(force_refresh=True)
207
210
  if latest:
@@ -255,12 +258,13 @@ class MainWindow(InspectorShell):
255
258
  self.update_indicator.mousePressEvent = self._on_update_indicator_clicked
256
259
 
257
260
  # Check for updates on launch
258
- from vector_inspector.services.update_service import UpdateService
259
- from vector_inspector.utils.version import get_app_version
260
261
  import threading
261
262
 
262
263
  from PySide6.QtCore import QTimer
263
264
 
265
+ from vector_inspector.services.update_service import UpdateService
266
+ from vector_inspector.utils.version import get_app_version
267
+
264
268
  def check_updates():
265
269
  latest = UpdateService.get_latest_release()
266
270
  if latest:
@@ -339,6 +343,28 @@ class MainWindow(InspectorShell):
339
343
  # Show update details dialog
340
344
  if not hasattr(self, "_latest_release"):
341
345
  return
346
+
347
+ # Track that user clicked on update available
348
+ try:
349
+ from vector_inspector.services.telemetry_service import TelemetryService
350
+ from vector_inspector.utils.version import get_app_version
351
+
352
+ telemetry = TelemetryService(self.settings_service)
353
+ if telemetry.is_enabled():
354
+ latest_version = self._latest_release.get("tag_name", "unknown")
355
+ event_data = {
356
+ "hwid": telemetry.get_hwid(),
357
+ "event_name": "update_clicked",
358
+ "app_version": get_app_version(),
359
+ "client_type": "vector-inspector",
360
+ "metadata": {"latest_version": latest_version},
361
+ }
362
+ telemetry.queue_event(event_data)
363
+ telemetry.send_batch()
364
+ except Exception as e:
365
+ # Don't let telemetry errors break the update flow
366
+ log_error(f"Telemetry error: {e}")
367
+
342
368
  DialogService.show_update_details(self._latest_release, self)
343
369
 
344
370
  def _connect_signals(self):
@@ -382,9 +408,8 @@ class MainWindow(InspectorShell):
382
408
 
383
409
  # Get active connection
384
410
  active = self.connection_manager.get_active_connection()
385
- conn = active.connection if active else None
386
411
 
387
- self.visualization_view = VisualizationView(conn)
412
+ self.visualization_view = VisualizationView(active)
388
413
  # Replace placeholder with actual view
389
414
  self.remove_main_tab(InspectorTabs.VISUALIZATION_TAB)
390
415
  self.add_main_tab(
@@ -405,7 +430,7 @@ class MainWindow(InspectorShell):
405
430
  self.breadcrumb_label.setText(instance.get_breadcrumb())
406
431
 
407
432
  # Update all views with new connection
408
- self._update_views_with_connection(instance.connection)
433
+ self._update_views_with_connection(instance)
409
434
 
410
435
  # If there's an active collection, update views with it
411
436
  if instance.active_collection:
@@ -455,7 +480,7 @@ class MainWindow(InspectorShell):
455
480
  # If this is the active connection, refresh the info panel
456
481
  if connection_id == self.connection_manager.get_active_connection_id():
457
482
  instance = self.connection_manager.get_connection(connection_id)
458
- if instance and instance.connection:
483
+ if instance and instance.is_connected:
459
484
  self.info_panel.refresh_database_info()
460
485
 
461
486
  def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
@@ -473,7 +498,7 @@ class MainWindow(InspectorShell):
473
498
  finally:
474
499
  self.connection_controller.loading_dialog.hide_loading()
475
500
 
476
- def _update_views_with_connection(self, connection: VectorDBConnection):
501
+ def _update_views_with_connection(self, connection: Optional[ConnectionInstance]):
477
502
  """Update all views with a new connection."""
478
503
  # Clear current collection when switching connections
479
504
  self.info_panel.current_collection = None
@@ -528,12 +553,12 @@ class MainWindow(InspectorShell):
528
553
  def _refresh_active_connection(self):
529
554
  """Refresh collections for the active connection."""
530
555
  active = self.connection_manager.get_active_connection()
531
- if not active or not active.connection.is_connected:
556
+ if not active or not active.is_connected:
532
557
  QMessageBox.information(self, "No Connection", "No active connection to refresh.")
533
558
  return
534
559
 
535
560
  try:
536
- collections = active.connection.list_collections()
561
+ collections = active.list_collections()
537
562
  self.connection_manager.update_collections(active.id, collections)
538
563
  self.statusBar().showMessage(f"Refreshed collections ({len(collections)} found)", 3000)
539
564
 
@@ -55,6 +55,7 @@ class DialogService:
55
55
 
56
56
  from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
57
57
 
58
+ # Pass through the connection object (ConnectionInstance expected by UI)
58
59
  dialog = BackupRestoreDialog(connection, collection_name or "", parent)
59
60
  return dialog.exec()
60
61
 
@@ -1,84 +1,88 @@
1
1
  """Collection browser for listing and selecting collections."""
2
2
 
3
+ from PySide6.QtCore import Qt, Signal
4
+ from PySide6.QtGui import QAction
3
5
  from PySide6.QtWidgets import (
4
- QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
5
- QGroupBox, QLabel, QMenu
6
+ QGroupBox,
7
+ QLabel,
8
+ QListWidget,
9
+ QListWidgetItem,
10
+ QMenu,
11
+ QVBoxLayout,
12
+ QWidget,
6
13
  )
7
- from PySide6.QtCore import Signal, Qt
8
- from PySide6.QtGui import QAction
9
14
 
10
- from vector_inspector.core.connections.base_connection import VectorDBConnection
15
+ from vector_inspector.core.connection_manager import ConnectionInstance
11
16
 
12
17
 
13
18
  class CollectionBrowser(QWidget):
14
19
  """Widget for browsing and selecting collections."""
15
-
20
+
16
21
  collection_selected = Signal(str)
17
-
18
- def __init__(self, connection: VectorDBConnection, parent=None):
22
+
23
+ def __init__(self, connection: Optional[ConnectionInstance] = None, parent=None):
19
24
  super().__init__(parent)
25
+ # Expects a ConnectionInstance wrapper.
20
26
  self.connection = connection
21
27
  self._setup_ui()
22
-
28
+
23
29
  def _setup_ui(self):
24
30
  """Setup widget UI."""
25
31
  layout = QVBoxLayout(self)
26
32
  layout.setContentsMargins(0, 0, 0, 0)
27
-
33
+
28
34
  group = QGroupBox("Collections")
29
35
  group_layout = QVBoxLayout()
30
-
36
+
31
37
  self.collection_list = QListWidget()
32
38
  self.collection_list.itemClicked.connect(self._on_collection_clicked)
33
39
  self.collection_list.setContextMenuPolicy(Qt.CustomContextMenu)
34
40
  self.collection_list.customContextMenuRequested.connect(self._show_context_menu)
35
-
41
+
36
42
  group_layout.addWidget(self.collection_list)
37
-
43
+
38
44
  self.info_label = QLabel("No collections")
39
45
  self.info_label.setWordWrap(True)
40
46
  self.info_label.setStyleSheet("color: gray; font-size: 10px;")
41
47
  group_layout.addWidget(self.info_label)
42
-
48
+
43
49
  group.setLayout(group_layout)
44
50
  layout.addWidget(group)
45
-
51
+
46
52
  def refresh(self):
47
53
  """Refresh collection list."""
48
54
  self.collection_list.clear()
49
-
55
+
50
56
  if not self.connection.is_connected:
51
57
  self.info_label.setText("Not connected")
52
58
  return
53
-
59
+
54
60
  collections = self.connection.list_collections()
55
-
61
+
56
62
  if not collections:
57
63
  # Show more context for persistent connections
58
64
  if self.connection.path:
59
- self.info_label.setText(
60
- f"No collections found at {self.connection.path}"
61
- )
65
+ self.info_label.setText(f"No collections found at {self.connection.path}")
62
66
  else:
63
67
  self.info_label.setText("No collections found")
64
68
  return
65
-
69
+
66
70
  for collection_name in collections:
67
71
  item = QListWidgetItem(collection_name)
68
72
  self.collection_list.addItem(item)
69
-
73
+
70
74
  self.info_label.setText(f"{len(collections)} collection(s)")
71
-
75
+
72
76
  def clear(self):
73
77
  """Clear collection list."""
74
78
  self.collection_list.clear()
75
79
  self.info_label.setText("No collections")
76
-
80
+
77
81
  def _on_collection_clicked(self, item: QListWidgetItem):
78
82
  """Handle collection selection."""
79
83
  collection_name = item.text()
80
84
  self.collection_selected.emit(collection_name)
81
-
85
+
82
86
  # Show collection info
83
87
  info = self.connection.get_collection_info(collection_name)
84
88
  if info:
@@ -87,24 +91,22 @@ class CollectionBrowser(QWidget):
87
91
  fields_str = ", ".join(fields[:3])
88
92
  if len(fields) > 3:
89
93
  fields_str += "..."
90
- self.info_label.setText(
91
- f"{count} items | Fields: {fields_str if fields else 'None'}"
92
- )
93
-
94
+ self.info_label.setText(f"{count} items | Fields: {fields_str if fields else 'None'}")
95
+
94
96
  def _show_context_menu(self, position):
95
97
  """Show context menu for collections."""
96
98
  item = self.collection_list.itemAt(position)
97
99
  if not item:
98
100
  return
99
-
101
+
100
102
  menu = QMenu(self)
101
-
103
+
102
104
  delete_action = QAction("Delete Collection", self)
103
105
  delete_action.triggered.connect(lambda: self._delete_collection(item.text()))
104
106
  menu.addAction(delete_action)
105
-
107
+
106
108
  menu.exec(self.collection_list.mapToGlobal(position))
107
-
109
+
108
110
  def _delete_collection(self, collection_name: str):
109
111
  """Delete a collection."""
110
112
  # TODO: Add confirmation dialog
@@ -1,5 +1,7 @@
1
1
  """Connection configuration view."""
2
2
 
3
+ from typing import Optional
4
+
3
5
  from PySide6.QtWidgets import (
4
6
  QWidget,
5
7
  QVBoxLayout,
@@ -437,8 +439,12 @@ class ConnectionView(QWidget):
437
439
  connection_changed = Signal(bool)
438
440
  connection_created = Signal(VectorDBConnection) # Signal when new connection is created
439
441
 
440
- def __init__(self, connection: VectorDBConnection, parent=None):
442
+ def __init__(self, connection: Optional[VectorDBConnection] = None, parent=None):
441
443
  super().__init__(parent)
444
+ # This view may be constructed without an active connection; it manages
445
+ # creation of `VectorDBConnection` instances. Keep `self.connection` as
446
+ # the low-level `VectorDBConnection` when present.
447
+ self._raw_connection = None
442
448
  self.connection = connection
443
449
  self.loading_dialog = LoadingDialog("Connecting to database...", self)
444
450
  self.settings_service = SettingsService()