vector-inspector 0.3.7__py3-none-any.whl → 0.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vector_inspector/core/connections/pgvector_connection.py +26 -1
- vector_inspector/core/provider_factory.py +97 -0
- vector_inspector/extensions/__init__.py +105 -0
- vector_inspector/services/settings_service.py +76 -0
- vector_inspector/ui/components/splash_window.py +14 -2
- vector_inspector/ui/controllers/__init__.py +1 -0
- vector_inspector/ui/controllers/connection_controller.py +177 -0
- vector_inspector/ui/dialogs/settings_dialog.py +124 -0
- vector_inspector/ui/main_window.py +222 -332
- vector_inspector/ui/main_window_shell.py +106 -0
- vector_inspector/ui/services/__init__.py +1 -0
- vector_inspector/ui/services/dialog_service.py +113 -0
- vector_inspector/ui/tabs.py +64 -0
- vector_inspector/ui/views/metadata_view.py +45 -0
- vector_inspector/ui/views/search_view.py +146 -13
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/METADATA +38 -9
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/RECORD +19 -10
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/WHEEL +0 -0
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Reusable UI shell for Vector Inspector applications."""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import (
|
|
4
|
+
QMainWindow,
|
|
5
|
+
QWidget,
|
|
6
|
+
QVBoxLayout,
|
|
7
|
+
QHBoxLayout,
|
|
8
|
+
QSplitter,
|
|
9
|
+
QTabWidget,
|
|
10
|
+
)
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InspectorShell(QMainWindow):
|
|
15
|
+
"""Base shell for Inspector applications with splitter, tab widget, and left panel.
|
|
16
|
+
|
|
17
|
+
This provides the basic UI structure that can be reused by Vector Inspector
|
|
18
|
+
and Vector Fusion Studio. Subclasses customize behavior and add domain logic.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
# Main UI components that subclasses will interact with
|
|
25
|
+
self.left_tabs = None
|
|
26
|
+
self.tab_widget = None
|
|
27
|
+
self.main_splitter = None
|
|
28
|
+
|
|
29
|
+
self._setup_shell_ui()
|
|
30
|
+
|
|
31
|
+
def _setup_shell_ui(self):
|
|
32
|
+
"""Setup the main UI shell layout."""
|
|
33
|
+
# Central widget with splitter
|
|
34
|
+
central_widget = QWidget()
|
|
35
|
+
self.setCentralWidget(central_widget)
|
|
36
|
+
|
|
37
|
+
layout = QHBoxLayout(central_widget)
|
|
38
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
|
39
|
+
|
|
40
|
+
# Main splitter (left panel | right tabs)
|
|
41
|
+
self.main_splitter = QSplitter(Qt.Horizontal)
|
|
42
|
+
|
|
43
|
+
# Left panel container (will hold tabs)
|
|
44
|
+
left_panel = QWidget()
|
|
45
|
+
left_layout = QVBoxLayout(left_panel)
|
|
46
|
+
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
47
|
+
|
|
48
|
+
# Create tab widget for left panel
|
|
49
|
+
self.left_tabs = QTabWidget()
|
|
50
|
+
left_layout.addWidget(self.left_tabs)
|
|
51
|
+
|
|
52
|
+
# Right panel - main content tabs
|
|
53
|
+
self.tab_widget = QTabWidget()
|
|
54
|
+
|
|
55
|
+
# Add panels to splitter
|
|
56
|
+
self.main_splitter.addWidget(left_panel)
|
|
57
|
+
self.main_splitter.addWidget(self.tab_widget)
|
|
58
|
+
self.main_splitter.setStretchFactor(0, 1)
|
|
59
|
+
self.main_splitter.setStretchFactor(1, 4)
|
|
60
|
+
|
|
61
|
+
layout.addWidget(self.main_splitter)
|
|
62
|
+
|
|
63
|
+
def add_left_panel(self, widget: QWidget, title: str, index: int = -1):
|
|
64
|
+
"""Add a panel to the left tab widget.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
widget: The panel widget to add
|
|
68
|
+
title: Display title for the tab
|
|
69
|
+
index: Optional position (default appends to end)
|
|
70
|
+
"""
|
|
71
|
+
if index < 0:
|
|
72
|
+
self.left_tabs.addTab(widget, title)
|
|
73
|
+
else:
|
|
74
|
+
self.left_tabs.insertTab(index, widget, title)
|
|
75
|
+
|
|
76
|
+
def add_main_tab(self, widget: QWidget, title: str, index: int = -1):
|
|
77
|
+
"""Add a tab to the main content area.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
widget: The tab widget to add
|
|
81
|
+
title: Display title for the tab
|
|
82
|
+
index: Optional position (default appends to end)
|
|
83
|
+
"""
|
|
84
|
+
if index < 0:
|
|
85
|
+
self.tab_widget.addTab(widget, title)
|
|
86
|
+
else:
|
|
87
|
+
self.tab_widget.insertTab(index, widget, title)
|
|
88
|
+
|
|
89
|
+
def set_left_panel_active(self, index: int):
|
|
90
|
+
"""Switch to a specific left panel tab."""
|
|
91
|
+
if 0 <= index < self.left_tabs.count():
|
|
92
|
+
self.left_tabs.setCurrentIndex(index)
|
|
93
|
+
|
|
94
|
+
def set_main_tab_active(self, index: int):
|
|
95
|
+
"""Switch to a specific main content tab."""
|
|
96
|
+
if 0 <= index < self.tab_widget.count():
|
|
97
|
+
self.tab_widget.setCurrentIndex(index)
|
|
98
|
+
|
|
99
|
+
def get_main_tab_count(self) -> int:
|
|
100
|
+
"""Get the number of main content tabs."""
|
|
101
|
+
return self.tab_widget.count()
|
|
102
|
+
|
|
103
|
+
def remove_main_tab(self, index: int):
|
|
104
|
+
"""Remove a main content tab."""
|
|
105
|
+
if 0 <= index < self.tab_widget.count():
|
|
106
|
+
self.tab_widget.removeTab(index)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""UI services for dialog management and utilities."""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Service for managing application dialogs."""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import QMessageBox, QDialog, QWidget
|
|
4
|
+
from vector_inspector.core.connection_manager import ConnectionManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DialogService:
|
|
8
|
+
"""Service for launching application dialogs."""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def show_about(parent: QWidget = None):
|
|
12
|
+
"""Show about dialog."""
|
|
13
|
+
from vector_inspector.utils.version import get_app_version
|
|
14
|
+
|
|
15
|
+
version = get_app_version()
|
|
16
|
+
version_html = (
|
|
17
|
+
f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
|
|
18
|
+
)
|
|
19
|
+
about_text = (
|
|
20
|
+
version_html + "<p>A comprehensive desktop application for visualizing, "
|
|
21
|
+
"querying, and managing multiple vector databases simultaneously.</p>"
|
|
22
|
+
'<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
23
|
+
"<hr />"
|
|
24
|
+
"<p>Built with PySide6</p>"
|
|
25
|
+
"<p><b>New:</b> Pinecone support!</p>"
|
|
26
|
+
)
|
|
27
|
+
QMessageBox.about(parent, "About Vector Inspector", about_text)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def show_backup_restore_dialog(
|
|
31
|
+
connection, collection_name: str = "", parent: QWidget = None
|
|
32
|
+
) -> int:
|
|
33
|
+
"""Show backup/restore dialog.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
connection: Active connection instance
|
|
37
|
+
collection_name: Optional collection name
|
|
38
|
+
parent: Parent widget
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
QDialog.Accepted or QDialog.Rejected
|
|
42
|
+
"""
|
|
43
|
+
if not connection:
|
|
44
|
+
QMessageBox.information(parent, "No Connection", "Please connect to a database first.")
|
|
45
|
+
return QDialog.Rejected
|
|
46
|
+
|
|
47
|
+
# Show info if no collection selected
|
|
48
|
+
if not collection_name:
|
|
49
|
+
QMessageBox.information(
|
|
50
|
+
parent,
|
|
51
|
+
"No Collection Selected",
|
|
52
|
+
"You can restore backups without a collection selected.\n"
|
|
53
|
+
"To create a backup, please select a collection first.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
|
|
57
|
+
|
|
58
|
+
dialog = BackupRestoreDialog(connection, collection_name or "", parent)
|
|
59
|
+
return dialog.exec()
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def show_migration_dialog(connection_manager: ConnectionManager, parent: QWidget = None) -> int:
|
|
63
|
+
"""Show cross-database migration dialog.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
connection_manager: Connection manager instance
|
|
67
|
+
parent: Parent widget
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
QDialog.Accepted or QDialog.Rejected
|
|
71
|
+
"""
|
|
72
|
+
if connection_manager.get_connection_count() < 2:
|
|
73
|
+
QMessageBox.information(
|
|
74
|
+
parent,
|
|
75
|
+
"Insufficient Connections",
|
|
76
|
+
"You need at least 2 active connections to migrate data.\n"
|
|
77
|
+
"Please connect to additional databases first.",
|
|
78
|
+
)
|
|
79
|
+
return QDialog.Rejected
|
|
80
|
+
|
|
81
|
+
from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
|
|
82
|
+
|
|
83
|
+
dialog = CrossDatabaseMigrationDialog(connection_manager, parent)
|
|
84
|
+
return dialog.exec()
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def show_profile_editor_prompt(parent: QWidget = None):
|
|
88
|
+
"""Show message prompting user to create a new profile."""
|
|
89
|
+
QMessageBox.information(
|
|
90
|
+
parent,
|
|
91
|
+
"Connect to Profile",
|
|
92
|
+
"Select a profile from the list and click 'Connect', or click '+' to create a new profile.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def show_update_details(latest_release: dict, parent: QWidget = None):
|
|
97
|
+
"""Show update details dialog.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
latest_release: Latest release info from GitHub API
|
|
101
|
+
parent: Parent widget
|
|
102
|
+
"""
|
|
103
|
+
from vector_inspector.ui.components.update_details_dialog import UpdateDetailsDialog
|
|
104
|
+
from vector_inspector.services.update_service import UpdateService
|
|
105
|
+
|
|
106
|
+
version = latest_release.get("tag_name", "?")
|
|
107
|
+
notes = latest_release.get("body", "")
|
|
108
|
+
instructions = UpdateService.get_update_instructions()
|
|
109
|
+
pip_cmd = instructions["pip"]
|
|
110
|
+
github_url = instructions["github"]
|
|
111
|
+
|
|
112
|
+
dialog = UpdateDetailsDialog(version, notes, pip_cmd, github_url, parent)
|
|
113
|
+
dialog.exec()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Tab registry for Inspector applications."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Tuple, Type
|
|
4
|
+
from PySide6.QtWidgets import QWidget
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TabDefinition:
|
|
8
|
+
"""Definition for a tab in the main content area."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, title: str, widget_class: Type[QWidget], lazy_load: bool = False):
|
|
11
|
+
self.title = title
|
|
12
|
+
self.widget_class = widget_class
|
|
13
|
+
self.lazy_load = lazy_load
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InspectorTabs:
|
|
17
|
+
"""Registry of standard Inspector tabs.
|
|
18
|
+
|
|
19
|
+
This allows both Vector Inspector and Vector Fusion Studio to use
|
|
20
|
+
the same tab definitions and add their own custom tabs.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Tab indices (for programmatic access)
|
|
24
|
+
INFO_TAB = 0
|
|
25
|
+
DATA_TAB = 1
|
|
26
|
+
SEARCH_TAB = 2
|
|
27
|
+
VISUALIZATION_TAB = 3
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def get_standard_tabs() -> List[TabDefinition]:
|
|
31
|
+
"""Get list of standard Inspector tabs.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of TabDefinition objects
|
|
35
|
+
"""
|
|
36
|
+
from vector_inspector.ui.views.info_panel import InfoPanel
|
|
37
|
+
from vector_inspector.ui.views.metadata_view import MetadataView
|
|
38
|
+
from vector_inspector.ui.views.search_view import SearchView
|
|
39
|
+
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
TabDefinition("Info", InfoPanel, lazy_load=False),
|
|
43
|
+
TabDefinition("Data Browser", MetadataView, lazy_load=False),
|
|
44
|
+
TabDefinition("Search", SearchView, lazy_load=False),
|
|
45
|
+
TabDefinition("Visualization", VisualizationView, lazy_load=True),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def create_tab_widget(tab_def: TabDefinition, connection=None) -> QWidget:
|
|
50
|
+
"""Create a widget instance from a tab definition.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
tab_def: Tab definition
|
|
54
|
+
connection: Optional connection to pass to widget
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Widget instance
|
|
58
|
+
"""
|
|
59
|
+
if tab_def.lazy_load:
|
|
60
|
+
# Return placeholder for lazy-loaded tabs
|
|
61
|
+
return QWidget()
|
|
62
|
+
else:
|
|
63
|
+
# Create widget with connection
|
|
64
|
+
return tab_def.widget_class(connection)
|
|
@@ -196,6 +196,9 @@ class MetadataView(QWidget):
|
|
|
196
196
|
self.table.setAlternatingRowColors(True)
|
|
197
197
|
self.table.horizontalHeader().setStretchLastSection(True)
|
|
198
198
|
self.table.doubleClicked.connect(self._on_row_double_clicked)
|
|
199
|
+
# Enable context menu
|
|
200
|
+
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
201
|
+
self.table.customContextMenuRequested.connect(self._show_context_menu)
|
|
199
202
|
layout.addWidget(self.table)
|
|
200
203
|
|
|
201
204
|
# Status bar
|
|
@@ -962,6 +965,48 @@ class MetadataView(QWidget):
|
|
|
962
965
|
else:
|
|
963
966
|
QMessageBox.warning(self, "Export Failed", "Failed to export data.")
|
|
964
967
|
|
|
968
|
+
def _show_context_menu(self, position):
|
|
969
|
+
"""Show context menu for table rows."""
|
|
970
|
+
# Get the item at the position
|
|
971
|
+
item = self.table.itemAt(position)
|
|
972
|
+
if not item:
|
|
973
|
+
return
|
|
974
|
+
|
|
975
|
+
row = item.row()
|
|
976
|
+
if row < 0 or row >= self.table.rowCount():
|
|
977
|
+
return
|
|
978
|
+
|
|
979
|
+
# Create context menu
|
|
980
|
+
menu = QMenu(self)
|
|
981
|
+
|
|
982
|
+
# Add standard "Edit" action
|
|
983
|
+
edit_action = menu.addAction("✏️ Edit")
|
|
984
|
+
edit_action.triggered.connect(
|
|
985
|
+
lambda: self._on_row_double_clicked(self.table.model().index(row, 0))
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
# Call extension hooks to add custom menu items
|
|
989
|
+
try:
|
|
990
|
+
from vector_inspector.extensions import table_context_menu_hook
|
|
991
|
+
|
|
992
|
+
table_context_menu_hook.trigger(
|
|
993
|
+
menu=menu,
|
|
994
|
+
table=self.table,
|
|
995
|
+
row=row,
|
|
996
|
+
data={
|
|
997
|
+
"current_data": self.current_data,
|
|
998
|
+
"collection_name": self.current_collection,
|
|
999
|
+
"database_name": self.current_database,
|
|
1000
|
+
"connection": self.connection,
|
|
1001
|
+
"view_type": "metadata",
|
|
1002
|
+
},
|
|
1003
|
+
)
|
|
1004
|
+
except Exception as e:
|
|
1005
|
+
log_info("Extension hook error: %s", e)
|
|
1006
|
+
|
|
1007
|
+
# Show menu
|
|
1008
|
+
menu.exec(self.table.viewport().mapToGlobal(position))
|
|
1009
|
+
|
|
965
1010
|
def _import_data(self, format_type: str):
|
|
966
1011
|
"""Import data from file into collection."""
|
|
967
1012
|
if not self.current_collection:
|
|
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import (
|
|
|
8
8
|
QTextEdit,
|
|
9
9
|
QPushButton,
|
|
10
10
|
QLabel,
|
|
11
|
+
QSizePolicy,
|
|
11
12
|
QSpinBox,
|
|
12
13
|
QTableWidget,
|
|
13
14
|
QTableWidgetItem,
|
|
@@ -15,8 +16,10 @@ from PySide6.QtWidgets import (
|
|
|
15
16
|
QSplitter,
|
|
16
17
|
QCheckBox,
|
|
17
18
|
QApplication,
|
|
19
|
+
QMenu,
|
|
18
20
|
)
|
|
19
21
|
from PySide6.QtCore import Qt
|
|
22
|
+
from PySide6.QtGui import QFontMetrics
|
|
20
23
|
|
|
21
24
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
22
25
|
from vector_inspector.ui.components.filter_builder import FilterBuilder
|
|
@@ -31,6 +34,19 @@ class SearchView(QWidget):
|
|
|
31
34
|
|
|
32
35
|
def __init__(self, connection: VectorDBConnection, parent=None):
|
|
33
36
|
super().__init__(parent)
|
|
37
|
+
# Initialize all UI attributes to None to avoid AttributeError
|
|
38
|
+
self.breadcrumb_label = None
|
|
39
|
+
self.query_input = None
|
|
40
|
+
self.results_table = None
|
|
41
|
+
self.results_status = None
|
|
42
|
+
self.refresh_button = None
|
|
43
|
+
self.n_results_spin = None
|
|
44
|
+
self.filter_builder = None
|
|
45
|
+
self.filter_group = None
|
|
46
|
+
self.search_button = None
|
|
47
|
+
self.loading_dialog = None
|
|
48
|
+
self.cache_manager = None
|
|
49
|
+
|
|
34
50
|
self.connection = connection
|
|
35
51
|
self.current_collection: str = ""
|
|
36
52
|
self.current_database: str = ""
|
|
@@ -42,8 +58,32 @@ class SearchView(QWidget):
|
|
|
42
58
|
|
|
43
59
|
def _setup_ui(self):
|
|
44
60
|
"""Setup widget UI."""
|
|
61
|
+
# Assign all UI attributes at the top to avoid NoneType errors
|
|
62
|
+
self.breadcrumb_label = QLabel("")
|
|
63
|
+
self.query_input = QTextEdit()
|
|
64
|
+
self.results_table = QTableWidget()
|
|
65
|
+
self.results_status = QLabel("No search performed")
|
|
66
|
+
self.refresh_button = QPushButton("Refresh")
|
|
67
|
+
self.n_results_spin = QSpinBox()
|
|
68
|
+
self.filter_builder = FilterBuilder()
|
|
69
|
+
self.filter_group = QGroupBox("Advanced Metadata Filters")
|
|
70
|
+
self.search_button = QPushButton("Search")
|
|
71
|
+
|
|
45
72
|
layout = QVBoxLayout(self)
|
|
46
73
|
|
|
74
|
+
# Breadcrumb bar (for pro features)
|
|
75
|
+
self.breadcrumb_label.setStyleSheet(
|
|
76
|
+
"color: #2980b9; font-weight: bold; padding: 2px 0 4px 0;"
|
|
77
|
+
)
|
|
78
|
+
# Configure breadcrumb label sizing
|
|
79
|
+
self.breadcrumb_label.setWordWrap(False)
|
|
80
|
+
self.breadcrumb_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
81
|
+
# Store full breadcrumb text for tooltip and eliding
|
|
82
|
+
self._full_breadcrumb = ""
|
|
83
|
+
# Elide mode: 'left' or 'middle'
|
|
84
|
+
self._elide_mode = "left"
|
|
85
|
+
layout.addWidget(self.breadcrumb_label)
|
|
86
|
+
|
|
47
87
|
# Create splitter for query and results
|
|
48
88
|
splitter = QSplitter(Qt.Vertical)
|
|
49
89
|
|
|
@@ -56,7 +96,6 @@ class SearchView(QWidget):
|
|
|
56
96
|
|
|
57
97
|
# Query input
|
|
58
98
|
query_group_layout.addWidget(QLabel("Enter search text:"))
|
|
59
|
-
self.query_input = QTextEdit()
|
|
60
99
|
self.query_input.setMaximumHeight(100)
|
|
61
100
|
self.query_input.setPlaceholderText("Enter text to search for similar vectors...")
|
|
62
101
|
query_group_layout.addWidget(self.query_input)
|
|
@@ -65,7 +104,6 @@ class SearchView(QWidget):
|
|
|
65
104
|
controls_layout = QHBoxLayout()
|
|
66
105
|
|
|
67
106
|
controls_layout.addWidget(QLabel("Results:"))
|
|
68
|
-
self.n_results_spin = QSpinBox()
|
|
69
107
|
self.n_results_spin.setMinimum(1)
|
|
70
108
|
self.n_results_spin.setMaximum(100)
|
|
71
109
|
self.n_results_spin.setValue(10)
|
|
@@ -73,9 +111,13 @@ class SearchView(QWidget):
|
|
|
73
111
|
|
|
74
112
|
controls_layout.addStretch()
|
|
75
113
|
|
|
76
|
-
self.search_button = QPushButton("Search")
|
|
77
114
|
self.search_button.clicked.connect(self._perform_search)
|
|
78
115
|
self.search_button.setDefault(True)
|
|
116
|
+
|
|
117
|
+
self.refresh_button.setToolTip("Reset search input and results")
|
|
118
|
+
self.refresh_button.clicked.connect(self._refresh_search)
|
|
119
|
+
controls_layout.addWidget(self.refresh_button)
|
|
120
|
+
|
|
79
121
|
controls_layout.addWidget(self.search_button)
|
|
80
122
|
|
|
81
123
|
query_group_layout.addLayout(controls_layout)
|
|
@@ -83,18 +125,15 @@ class SearchView(QWidget):
|
|
|
83
125
|
query_layout.addWidget(query_group)
|
|
84
126
|
|
|
85
127
|
# Advanced filters section
|
|
86
|
-
filter_group
|
|
87
|
-
filter_group.
|
|
88
|
-
filter_group.setChecked(False)
|
|
128
|
+
self.filter_group.setCheckable(True)
|
|
129
|
+
self.filter_group.setChecked(False)
|
|
89
130
|
filter_group_layout = QVBoxLayout()
|
|
90
131
|
|
|
91
|
-
# Filter builder
|
|
92
|
-
self.filter_builder = FilterBuilder()
|
|
132
|
+
# Filter builder (already created at top)
|
|
93
133
|
filter_group_layout.addWidget(self.filter_builder)
|
|
94
134
|
|
|
95
|
-
filter_group.setLayout(filter_group_layout)
|
|
96
|
-
query_layout.addWidget(filter_group)
|
|
97
|
-
self.filter_group = filter_group
|
|
135
|
+
self.filter_group.setLayout(filter_group_layout)
|
|
136
|
+
query_layout.addWidget(self.filter_group)
|
|
98
137
|
|
|
99
138
|
splitter.addWidget(query_widget)
|
|
100
139
|
|
|
@@ -106,12 +145,13 @@ class SearchView(QWidget):
|
|
|
106
145
|
results_group = QGroupBox("Search Results")
|
|
107
146
|
results_group_layout = QVBoxLayout()
|
|
108
147
|
|
|
109
|
-
self.results_table = QTableWidget()
|
|
110
148
|
self.results_table.setSelectionBehavior(QTableWidget.SelectRows)
|
|
111
149
|
self.results_table.setAlternatingRowColors(True)
|
|
150
|
+
# Enable context menu
|
|
151
|
+
self.results_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
152
|
+
self.results_table.customContextMenuRequested.connect(self._show_context_menu)
|
|
112
153
|
results_group_layout.addWidget(self.results_table)
|
|
113
154
|
|
|
114
|
-
self.results_status = QLabel("No search performed")
|
|
115
155
|
self.results_status.setStyleSheet("color: gray;")
|
|
116
156
|
results_group_layout.addWidget(self.results_status)
|
|
117
157
|
|
|
@@ -125,6 +165,57 @@ class SearchView(QWidget):
|
|
|
125
165
|
splitter.setStretchFactor(1, 2)
|
|
126
166
|
|
|
127
167
|
layout.addWidget(splitter)
|
|
168
|
+
self.setLayout(layout)
|
|
169
|
+
|
|
170
|
+
def set_breadcrumb(self, text: str):
|
|
171
|
+
"""Set the breadcrumb indicator (for pro features)."""
|
|
172
|
+
# Keep the full breadcrumb for tooltip and compute an elided
|
|
173
|
+
# display that fits the current label width (elide from the left).
|
|
174
|
+
self._full_breadcrumb = text or ""
|
|
175
|
+
self.breadcrumb_label.setToolTip(self._full_breadcrumb)
|
|
176
|
+
self._update_breadcrumb_display()
|
|
177
|
+
|
|
178
|
+
def _update_breadcrumb_display(self):
|
|
179
|
+
"""Compute and apply an elided breadcrumb display based on label width."""
|
|
180
|
+
if not hasattr(self, "breadcrumb_label") or self.breadcrumb_label is None:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
fm = QFontMetrics(self.breadcrumb_label.font())
|
|
184
|
+
avail_width = max(10, self.breadcrumb_label.width())
|
|
185
|
+
if not self._full_breadcrumb:
|
|
186
|
+
self.breadcrumb_label.setText("")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Choose elide mode from settings
|
|
190
|
+
elide_flag = Qt.ElideLeft if self._elide_mode == "left" else Qt.ElideMiddle
|
|
191
|
+
elided = fm.elidedText(self._full_breadcrumb, elide_flag, avail_width)
|
|
192
|
+
self.breadcrumb_label.setText(elided)
|
|
193
|
+
|
|
194
|
+
def set_elide_mode(self, mode: str):
|
|
195
|
+
"""Set elide mode ('left' or 'middle') and refresh display."""
|
|
196
|
+
if mode not in ("left", "middle"):
|
|
197
|
+
mode = "left"
|
|
198
|
+
self._elide_mode = mode
|
|
199
|
+
self._update_breadcrumb_display()
|
|
200
|
+
|
|
201
|
+
def resizeEvent(self, event):
|
|
202
|
+
"""Handle resize to recompute breadcrumb eliding."""
|
|
203
|
+
try:
|
|
204
|
+
super().resizeEvent(event)
|
|
205
|
+
finally:
|
|
206
|
+
self._update_breadcrumb_display()
|
|
207
|
+
|
|
208
|
+
def clear_breadcrumb(self):
|
|
209
|
+
"""Clear the breadcrumb indicator."""
|
|
210
|
+
self.breadcrumb_label.setText("")
|
|
211
|
+
|
|
212
|
+
def _refresh_search(self):
|
|
213
|
+
"""Reset search input, results, and breadcrumb."""
|
|
214
|
+
self.query_input.clear()
|
|
215
|
+
self.results_table.setRowCount(0)
|
|
216
|
+
self.results_status.setText("No search performed")
|
|
217
|
+
self.clear_breadcrumb()
|
|
218
|
+
self.search_results = None
|
|
128
219
|
|
|
129
220
|
def set_collection(self, collection_name: str, database_name: str = ""):
|
|
130
221
|
"""Set the current collection to search."""
|
|
@@ -139,6 +230,11 @@ class SearchView(QWidget):
|
|
|
139
230
|
collection_name,
|
|
140
231
|
)
|
|
141
232
|
|
|
233
|
+
# Guard: if results_table is not yet initialized, do nothing
|
|
234
|
+
if self.results_table is None:
|
|
235
|
+
log_info("[SearchView] set_collection called before UI setup; skipping.")
|
|
236
|
+
return
|
|
237
|
+
|
|
142
238
|
# Check cache first
|
|
143
239
|
cached = self.cache_manager.get(self.current_database, self.current_collection)
|
|
144
240
|
if cached:
|
|
@@ -281,6 +377,43 @@ class SearchView(QWidget):
|
|
|
281
377
|
},
|
|
282
378
|
)
|
|
283
379
|
|
|
380
|
+
def _show_context_menu(self, position):
|
|
381
|
+
"""Show context menu for results table rows."""
|
|
382
|
+
# Get the item at the position
|
|
383
|
+
item = self.results_table.itemAt(position)
|
|
384
|
+
if not item:
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
row = item.row()
|
|
388
|
+
if row < 0 or row >= self.results_table.rowCount():
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
# Create context menu
|
|
392
|
+
menu = QMenu(self)
|
|
393
|
+
|
|
394
|
+
# Call extension hooks to add custom menu items
|
|
395
|
+
try:
|
|
396
|
+
from vector_inspector.extensions import table_context_menu_hook
|
|
397
|
+
|
|
398
|
+
table_context_menu_hook.trigger(
|
|
399
|
+
menu=menu,
|
|
400
|
+
table=self.results_table,
|
|
401
|
+
row=row,
|
|
402
|
+
data={
|
|
403
|
+
"current_data": self.search_results,
|
|
404
|
+
"collection_name": self.current_collection,
|
|
405
|
+
"database_name": self.current_database,
|
|
406
|
+
"connection": self.connection,
|
|
407
|
+
"view_type": "search",
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
except Exception as e:
|
|
411
|
+
log_info("Extension hook error: %s", e)
|
|
412
|
+
|
|
413
|
+
# Only show menu if it has items
|
|
414
|
+
if not menu.isEmpty():
|
|
415
|
+
menu.exec(self.results_table.viewport().mapToGlobal(position))
|
|
416
|
+
|
|
284
417
|
def _display_results(self, results: Dict[str, Any]):
|
|
285
418
|
"""Display search results in table."""
|
|
286
419
|
ids = results.get("ids", [[]])[0]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: vector-inspector
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
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
|
|
@@ -31,13 +31,40 @@ Requires-Dist: pgvector>=0.4.2
|
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
33
33
|
# Latest updates
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
|
|
35
|
+
- Major refactor and studio-ready architecture
|
|
36
|
+
- Refactored main window into modular components (InspectorShell, ProviderFactory, DialogService, ConnectionController, InspectorTabs)
|
|
37
|
+
- MainWindow is reusable as a widget; tab system is pluggable so Studio can extend or override tabs
|
|
38
|
+
|
|
39
|
+
- Data browser improvements
|
|
40
|
+
- Added Generate embeddings on edit (persisted per user)
|
|
41
|
+
|
|
42
|
+
- Settings / Preferences
|
|
43
|
+
- SettingsService persists preferences and exposes typed accessors (breadcrumb, search defaults, auto-embed, window geometry)
|
|
44
|
+
- SettingsService emits a setting_changed Qt signal so UI reacts immediately
|
|
45
|
+
- SettingsDialog (modal) added with add_section API and hook integration for extension panels
|
|
46
|
+
- Breadcrumb controls moved out of core so Pro (Vector Studio) injects them via the settings_panel_hook
|
|
47
|
+
|
|
48
|
+
- Extension hook for settings panels
|
|
49
|
+
- *settings_panel_hook* added to *vector_inspector.extensions*; Vector Studio registers breadcrumb controls at startup
|
|
50
|
+
|
|
51
|
+
- Breadcrumb and UI improvements
|
|
52
|
+
- Breadcrumb label now elides long trails (left/middle) and shows full trail on hover
|
|
53
|
+
- SearchView supports runtime elide-mode changes and responds to settings signals
|
|
54
|
+
|
|
55
|
+
- Window geometry persistence
|
|
56
|
+
- Main window saves/restores geometry when window.restore_geometry is enabled
|
|
57
|
+
|
|
58
|
+
- Pro (Vector Studio) features
|
|
59
|
+
- *Search Similar* (Pro): right-click any row in Data Browser or Search Results to run a vector-to-vector similarity search
|
|
60
|
+
- *table_context_menu* handler hardened for many embedding/id formats and includes fallbacks
|
|
61
|
+
- Vector Studio injects breadcrumb controls into Settings dialog via *settings_panel_hook*
|
|
62
|
+
|
|
63
|
+
- Tests and CI
|
|
64
|
+
- Added *tests/test_settings_injection.py* to assert settings panel hook registration
|
|
65
|
+
- Updated context-menu tests to use *log_info* and *assert* for pytest
|
|
66
|
+
- Local test run: 5 tests passed; GUI-heavy suite ~9s due to PySide6 startup
|
|
67
|
+
|
|
41
68
|
---
|
|
42
69
|
|
|
43
70
|
# Vector Inspector
|
|
@@ -45,7 +72,7 @@ Description-Content-Type: text/markdown
|
|
|
45
72
|
> **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested and code is released frequently. Use with caution in critical or production environments.
|
|
46
73
|
|
|
47
74
|
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/ci-tests.yml)
|
|
48
|
-
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/publish.yml)
|
|
75
|
+
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/publish%20copy.yml)
|
|
49
76
|
|
|
50
77
|
[](https://pypi.org/project/vector-inspector/)
|
|
51
78
|
[](https://pepy.tech/projects/vector-inspector)
|
|
@@ -74,6 +101,8 @@ Vector Inspector bridges the gap between vector databases and user-friendly data
|
|
|
74
101
|
|
|
75
102
|
## Key Features
|
|
76
103
|
|
|
104
|
+
> **Note:** Some features listed below may be not started or currently in progress.
|
|
105
|
+
|
|
77
106
|
### 1. **Multi-Provider Support**
|
|
78
107
|
- Connect to vector databases:
|
|
79
108
|
- ChromaDB (persistent local storage)
|