vector-inspector 0.3.8__py3-none-any.whl → 0.3.11__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/__init__.py +10 -1
- vector_inspector/core/connections/pgvector_connection.py +26 -1
- vector_inspector/extensions/__init__.py +111 -0
- vector_inspector/extensions/telemetry_settings_panel.py +25 -0
- vector_inspector/main.py +16 -0
- vector_inspector/services/settings_service.py +84 -0
- vector_inspector/services/telemetry_service.py +88 -0
- vector_inspector/ui/dialogs/settings_dialog.py +124 -0
- vector_inspector/ui/main_window.py +138 -1
- vector_inspector/ui/views/metadata_view.py +45 -0
- vector_inspector/ui/views/search_view.py +146 -13
- {vector_inspector-0.3.8.dist-info → vector_inspector-0.3.11.dist-info}/METADATA +7 -27
- {vector_inspector-0.3.8.dist-info → vector_inspector-0.3.11.dist-info}/RECORD +15 -11
- {vector_inspector-0.3.8.dist-info → vector_inspector-0.3.11.dist-info}/WHEEL +1 -1
- {vector_inspector-0.3.8.dist-info → vector_inspector-0.3.11.dist-info}/entry_points.txt +0 -0
vector_inspector/__init__.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
"""Vector Inspector - A comprehensive desktop application for vector database visualization."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.
|
|
3
|
+
__version__ = "0.3.11" # Keep in sync with pyproject.toml for dev mode fallback
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_version():
|
|
7
|
+
try:
|
|
8
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
9
|
+
|
|
10
|
+
return version("vector-inspector")
|
|
11
|
+
except Exception:
|
|
12
|
+
return __version__
|
|
@@ -5,6 +5,8 @@ import json
|
|
|
5
5
|
import psycopg2
|
|
6
6
|
from psycopg2 import sql
|
|
7
7
|
|
|
8
|
+
from vector_inspector.core.logging import log_info
|
|
9
|
+
|
|
8
10
|
## No need to import register_vector; pgvector extension is enabled at table creation
|
|
9
11
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
10
12
|
from vector_inspector.core.logging import log_error, log_info
|
|
@@ -1043,12 +1045,35 @@ class PgVectorConnection(VectorDBConnection):
|
|
|
1043
1045
|
Returns:
|
|
1044
1046
|
List of floats
|
|
1045
1047
|
"""
|
|
1048
|
+
# log_info("[pgvector] _parse_vector raw value: %r (type: %s)", vector_str, type(vector_str))
|
|
1046
1049
|
if isinstance(vector_str, list):
|
|
1050
|
+
log_info("[pgvector] _parse_vector: already list, len=%d", len(vector_str))
|
|
1047
1051
|
return vector_str
|
|
1052
|
+
# Handle numpy arrays
|
|
1053
|
+
try:
|
|
1054
|
+
import numpy as np
|
|
1055
|
+
|
|
1056
|
+
if isinstance(vector_str, np.ndarray):
|
|
1057
|
+
log_info("[pgvector] _parse_vector: numpy array, shape=%s", vector_str.shape)
|
|
1058
|
+
return vector_str.tolist()
|
|
1059
|
+
except ImportError:
|
|
1060
|
+
pass
|
|
1048
1061
|
if isinstance(vector_str, str):
|
|
1049
1062
|
# Remove brackets and split by comma
|
|
1050
1063
|
vector_str = vector_str.strip("[]")
|
|
1051
|
-
|
|
1064
|
+
if not vector_str:
|
|
1065
|
+
log_info("[pgvector] _parse_vector: empty string after strip")
|
|
1066
|
+
return []
|
|
1067
|
+
try:
|
|
1068
|
+
parsed = [float(x) for x in vector_str.split(",")]
|
|
1069
|
+
log_info("[pgvector] _parse_vector: parsed list of len %d", len(parsed))
|
|
1070
|
+
return parsed
|
|
1071
|
+
except Exception as e:
|
|
1072
|
+
log_info(
|
|
1073
|
+
"[pgvector] _parse_vector: failed to parse '%s' with error: %s", vector_str, e
|
|
1074
|
+
)
|
|
1075
|
+
return []
|
|
1076
|
+
log_info("[pgvector] _parse_vector: unhandled type %s, returning []", type(vector_str))
|
|
1052
1077
|
return []
|
|
1053
1078
|
|
|
1054
1079
|
def compute_embeddings_for_documents(
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Extension points for Vector Inspector.
|
|
2
|
+
|
|
3
|
+
This module provides hooks and callbacks that allow pro versions
|
|
4
|
+
or plugins to extend core functionality without modifying the base code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, ClassVar
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from PySide6.QtWidgets import QMenu, QTableWidget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TableContextMenuHook:
|
|
13
|
+
"""Hook for adding custom context menu items to table widgets."""
|
|
14
|
+
|
|
15
|
+
_handlers: ClassVar[list[Callable]] = []
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def register(cls, handler: Callable):
|
|
19
|
+
"""Register a context menu handler.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
handler: Callable that takes (menu: QMenu, table: QTableWidget, row: int, data: Dict)
|
|
23
|
+
and adds menu items to the menu.
|
|
24
|
+
"""
|
|
25
|
+
if handler not in cls._handlers:
|
|
26
|
+
cls._handlers.append(handler)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def unregister(cls, handler: Callable):
|
|
30
|
+
"""Unregister a context menu handler."""
|
|
31
|
+
if handler in cls._handlers:
|
|
32
|
+
cls._handlers.remove(handler)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def trigger(
|
|
36
|
+
cls,
|
|
37
|
+
menu: QMenu,
|
|
38
|
+
table: QTableWidget,
|
|
39
|
+
row: int,
|
|
40
|
+
data: dict[str, Any] | None = None,
|
|
41
|
+
):
|
|
42
|
+
"""Trigger all registered handlers.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
menu: The QMenu to add items to
|
|
46
|
+
table: The QTableWidget that was right-clicked
|
|
47
|
+
row: The row number that was clicked
|
|
48
|
+
data: Optional data dictionary with context (ids, documents, metadatas, etc.)
|
|
49
|
+
"""
|
|
50
|
+
for handler in cls._handlers:
|
|
51
|
+
try:
|
|
52
|
+
handler(menu, table, row, data)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
# Log but don't break if a handler fails
|
|
55
|
+
from vector_inspector.core.logging import log_error
|
|
56
|
+
|
|
57
|
+
log_error("Context menu handler error: %s", e)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def clear(cls):
|
|
61
|
+
"""Clear all registered handlers."""
|
|
62
|
+
cls._handlers.clear()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Global singleton instance
|
|
66
|
+
table_context_menu_hook = TableContextMenuHook()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SettingsPanelHook:
|
|
70
|
+
"""Hook for adding custom sections to the Settings/Preferences dialog."""
|
|
71
|
+
|
|
72
|
+
_handlers: ClassVar[list[Callable]] = []
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def register(cls, handler: Callable):
|
|
76
|
+
"""Register a settings panel provider.
|
|
77
|
+
|
|
78
|
+
Handler signature: (parent_layout, settings_service, dialog)
|
|
79
|
+
where `parent_layout` is a QLayout the handler can add widgets to.
|
|
80
|
+
"""
|
|
81
|
+
if handler not in cls._handlers:
|
|
82
|
+
cls._handlers.append(handler)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def unregister(cls, handler: Callable):
|
|
86
|
+
if handler in cls._handlers:
|
|
87
|
+
cls._handlers.remove(handler)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def trigger(cls, parent_layout, settings_service, dialog=None):
|
|
91
|
+
for handler in cls._handlers:
|
|
92
|
+
try:
|
|
93
|
+
handler(parent_layout, settings_service, dialog)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
from vector_inspector.core.logging import log_error
|
|
96
|
+
|
|
97
|
+
log_error("Settings panel handler error: %s", e)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def clear(cls):
|
|
101
|
+
cls._handlers.clear()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Global singleton instance
|
|
105
|
+
settings_panel_hook = SettingsPanelHook()
|
|
106
|
+
|
|
107
|
+
# Register built-in settings panel extensions
|
|
108
|
+
try:
|
|
109
|
+
import vector_inspector.extensions.telemetry_settings_panel
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from PySide6.QtWidgets import QCheckBox, QHBoxLayout
|
|
2
|
+
from vector_inspector.extensions import settings_panel_hook
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def add_telemetry_section(parent_layout, settings_service, dialog=None):
|
|
6
|
+
telemetry_checkbox = QCheckBox("Enable anonymous telemetry (app launch events)")
|
|
7
|
+
# Default to checked if not set
|
|
8
|
+
checked = settings_service.get("telemetry.enabled")
|
|
9
|
+
if checked is None:
|
|
10
|
+
checked = True
|
|
11
|
+
telemetry_checkbox.setChecked(checked)
|
|
12
|
+
telemetry_checkbox.setToolTip(
|
|
13
|
+
"Allow the app to send anonymous launch events to help improve reliability. No personal or document data is sent."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def on_state_changed(state):
|
|
17
|
+
settings_service.set_telemetry_enabled(bool(state))
|
|
18
|
+
|
|
19
|
+
telemetry_checkbox.stateChanged.connect(on_state_changed)
|
|
20
|
+
layout = QHBoxLayout()
|
|
21
|
+
layout.addWidget(telemetry_checkbox)
|
|
22
|
+
parent_layout.addLayout(layout)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
settings_panel_hook.register(add_telemetry_section)
|
vector_inspector/main.py
CHANGED
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import os
|
|
5
5
|
from PySide6.QtWidgets import QApplication
|
|
6
6
|
from vector_inspector.ui.main_window import MainWindow
|
|
7
|
+
from vector_inspector.core.logging import log_error
|
|
7
8
|
|
|
8
9
|
# Ensures the app looks in its own folder for the raw libraries
|
|
9
10
|
sys.path.append(os.path.dirname(sys.executable))
|
|
@@ -15,6 +16,21 @@ def main():
|
|
|
15
16
|
app.setApplicationName("Vector Inspector")
|
|
16
17
|
app.setOrganizationName("Vector Inspector")
|
|
17
18
|
|
|
19
|
+
# Telemetry: send launch ping if enabled
|
|
20
|
+
try:
|
|
21
|
+
from vector_inspector.services.telemetry_service import TelemetryService
|
|
22
|
+
from vector_inspector import get_version, __version__
|
|
23
|
+
|
|
24
|
+
app_version = None
|
|
25
|
+
try:
|
|
26
|
+
app_version = get_version()
|
|
27
|
+
except Exception:
|
|
28
|
+
app_version = __version__
|
|
29
|
+
telemetry = TelemetryService()
|
|
30
|
+
telemetry.send_launch_ping(app_version=app_version)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
log_error(f"[Telemetry] Failed to send launch ping: {e}")
|
|
33
|
+
|
|
18
34
|
window = MainWindow()
|
|
19
35
|
window.show()
|
|
20
36
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Service for persisting application settings."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import base64
|
|
5
|
+
from PySide6.QtCore import QObject, Signal
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Dict, Any, Optional, List
|
|
6
8
|
from vector_inspector.core.cache_manager import invalidate_cache_on_settings_change
|
|
@@ -12,6 +14,18 @@ class SettingsService:
|
|
|
12
14
|
|
|
13
15
|
def __init__(self):
|
|
14
16
|
"""Initialize settings service."""
|
|
17
|
+
|
|
18
|
+
# Expose a shared QObject-based signal emitter so UI can react to
|
|
19
|
+
# settings changes without polling.
|
|
20
|
+
class _Signals(QObject):
|
|
21
|
+
setting_changed = Signal(str, object)
|
|
22
|
+
|
|
23
|
+
# singleton-like per-process signals instance
|
|
24
|
+
try:
|
|
25
|
+
self.signals
|
|
26
|
+
except Exception:
|
|
27
|
+
self.signals = _Signals()
|
|
28
|
+
|
|
15
29
|
self.settings_dir = Path.home() / ".vector-inspector"
|
|
16
30
|
self.settings_file = self.settings_dir / "settings.json"
|
|
17
31
|
self.settings: Dict[str, Any] = {}
|
|
@@ -51,6 +65,62 @@ class SettingsService:
|
|
|
51
65
|
"""Get a setting value."""
|
|
52
66
|
return self.settings.get(key, default)
|
|
53
67
|
|
|
68
|
+
# Convenience accessors for common settings
|
|
69
|
+
def get_breadcrumb_enabled(self) -> bool:
|
|
70
|
+
return bool(self.settings.get("breadcrumb.enabled", True))
|
|
71
|
+
|
|
72
|
+
def set_breadcrumb_enabled(self, enabled: bool):
|
|
73
|
+
self.set("breadcrumb.enabled", bool(enabled))
|
|
74
|
+
|
|
75
|
+
def get_breadcrumb_elide_mode(self) -> str:
|
|
76
|
+
return str(self.settings.get("breadcrumb.elide_mode", "left"))
|
|
77
|
+
|
|
78
|
+
def set_breadcrumb_elide_mode(self, mode: str):
|
|
79
|
+
if mode not in ("left", "middle"):
|
|
80
|
+
mode = "left"
|
|
81
|
+
self.set("breadcrumb.elide_mode", mode)
|
|
82
|
+
|
|
83
|
+
def get_default_n_results(self) -> int:
|
|
84
|
+
return int(self.settings.get("search.default_n_results", 10))
|
|
85
|
+
|
|
86
|
+
def set_default_n_results(self, n: int):
|
|
87
|
+
self.set("search.default_n_results", int(n))
|
|
88
|
+
|
|
89
|
+
def get_auto_generate_embeddings(self) -> bool:
|
|
90
|
+
return bool(self.settings.get("embeddings.auto_generate", True))
|
|
91
|
+
|
|
92
|
+
def set_auto_generate_embeddings(self, enabled: bool):
|
|
93
|
+
self.set("embeddings.auto_generate", bool(enabled))
|
|
94
|
+
|
|
95
|
+
def get_window_restore_geometry(self) -> bool:
|
|
96
|
+
return bool(self.settings.get("window.restore_geometry", True))
|
|
97
|
+
|
|
98
|
+
def set_window_restore_geometry(self, enabled: bool):
|
|
99
|
+
self.set("window.restore_geometry", bool(enabled))
|
|
100
|
+
|
|
101
|
+
def set_window_geometry(self, geometry_bytes: bytes):
|
|
102
|
+
"""Save window geometry as base64 string."""
|
|
103
|
+
try:
|
|
104
|
+
if isinstance(geometry_bytes, str):
|
|
105
|
+
# assume base64 already
|
|
106
|
+
b64 = geometry_bytes
|
|
107
|
+
else:
|
|
108
|
+
b64 = base64.b64encode(bytes(geometry_bytes)).decode("ascii")
|
|
109
|
+
self.set("window.geometry", b64)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
log_error("Failed to set window geometry: %s", e)
|
|
112
|
+
|
|
113
|
+
def get_window_geometry(self) -> Optional[bytes]:
|
|
114
|
+
"""Return geometry bytes or None."""
|
|
115
|
+
try:
|
|
116
|
+
b64 = self.settings.get("window.geometry")
|
|
117
|
+
if not b64:
|
|
118
|
+
return None
|
|
119
|
+
return base64.b64decode(b64)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
log_error("Failed to get window geometry: %s", e)
|
|
122
|
+
return None
|
|
123
|
+
|
|
54
124
|
def get_cache_enabled(self) -> bool:
|
|
55
125
|
"""Get whether caching is enabled (default: True)."""
|
|
56
126
|
return self.settings.get("cache_enabled", True)
|
|
@@ -67,6 +137,14 @@ class SettingsService:
|
|
|
67
137
|
else:
|
|
68
138
|
cache.disable()
|
|
69
139
|
|
|
140
|
+
def get_telemetry_enabled(self) -> bool:
|
|
141
|
+
"""Get whether telemetry is enabled (default: True)."""
|
|
142
|
+
return bool(self.settings.get("telemetry.enabled", True))
|
|
143
|
+
|
|
144
|
+
def set_telemetry_enabled(self, enabled: bool):
|
|
145
|
+
"""Set whether telemetry is enabled."""
|
|
146
|
+
self.set("telemetry.enabled", bool(enabled))
|
|
147
|
+
|
|
70
148
|
def set(self, key: str, value: Any):
|
|
71
149
|
"""Set a setting value."""
|
|
72
150
|
self.settings[key] = value
|
|
@@ -74,6 +152,12 @@ class SettingsService:
|
|
|
74
152
|
# Invalidate cache when settings change (only if cache is enabled)
|
|
75
153
|
if key != "cache_enabled": # Don't invalidate when toggling cache itself
|
|
76
154
|
invalidate_cache_on_settings_change()
|
|
155
|
+
# Emit change signal for UI/reactive components
|
|
156
|
+
try:
|
|
157
|
+
# Emit the raw python object (value) for convenience
|
|
158
|
+
self.signals.setting_changed.emit(key, value)
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
77
161
|
|
|
78
162
|
def clear(self):
|
|
79
163
|
"""Clear all settings."""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import platform
|
|
3
|
+
import uuid
|
|
4
|
+
import requests
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from vector_inspector.services.settings_service import SettingsService
|
|
7
|
+
from vector_inspector.core.logging import log_info, log_error
|
|
8
|
+
|
|
9
|
+
TELEMETRY_ENDPOINT = "https://api.divinedevops.com/api/v1/telemetry"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TelemetryService:
|
|
13
|
+
def __init__(self, settings_service=None):
|
|
14
|
+
self.settings = settings_service or SettingsService()
|
|
15
|
+
self.queue_file = Path.home() / ".vector-inspector" / "telemetry_queue.json"
|
|
16
|
+
self._load_queue()
|
|
17
|
+
|
|
18
|
+
def _load_queue(self):
|
|
19
|
+
if self.queue_file.exists():
|
|
20
|
+
try:
|
|
21
|
+
with open(self.queue_file, encoding="utf-8") as f:
|
|
22
|
+
self.queue = json.load(f)
|
|
23
|
+
except Exception:
|
|
24
|
+
self.queue = []
|
|
25
|
+
else:
|
|
26
|
+
self.queue = []
|
|
27
|
+
|
|
28
|
+
def _save_queue(self):
|
|
29
|
+
self.queue_file.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
with open(self.queue_file, "w", encoding="utf-8") as f:
|
|
31
|
+
json.dump(self.queue, f, indent=2)
|
|
32
|
+
|
|
33
|
+
def is_enabled(self):
|
|
34
|
+
return bool(self.settings.get("telemetry.enabled", True))
|
|
35
|
+
|
|
36
|
+
def get_hwid(self):
|
|
37
|
+
# Use a persistent UUID for this client
|
|
38
|
+
hwid = self.settings.get("telemetry.hwid")
|
|
39
|
+
if not hwid:
|
|
40
|
+
hwid = str(uuid.uuid4())
|
|
41
|
+
self.settings.set("telemetry.hwid", hwid)
|
|
42
|
+
return hwid
|
|
43
|
+
|
|
44
|
+
def queue_event(self, event):
|
|
45
|
+
self.queue.append(event)
|
|
46
|
+
self._save_queue()
|
|
47
|
+
|
|
48
|
+
def send_batch(self):
|
|
49
|
+
if not self.is_enabled() or not self.queue:
|
|
50
|
+
return
|
|
51
|
+
sent = []
|
|
52
|
+
for event in self.queue:
|
|
53
|
+
try:
|
|
54
|
+
log_info(
|
|
55
|
+
f"[Telemetry] Sending to {TELEMETRY_ENDPOINT}\nPayload: {json.dumps(event, indent=2)}"
|
|
56
|
+
)
|
|
57
|
+
resp = requests.post(TELEMETRY_ENDPOINT, json=event, timeout=5)
|
|
58
|
+
log_info(f"[Telemetry] Response: {resp.status_code} {resp.text}")
|
|
59
|
+
if resp.status_code in (200, 201):
|
|
60
|
+
sent.append(event)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
log_error(f"[Telemetry] Exception: {e}")
|
|
63
|
+
# Remove sent events
|
|
64
|
+
self.queue = [e for e in self.queue if e not in sent]
|
|
65
|
+
self._save_queue()
|
|
66
|
+
|
|
67
|
+
def send_launch_ping(self, app_version, client_type="vector-inspector"):
|
|
68
|
+
log_info("[Telemetry] send_launch_ping called")
|
|
69
|
+
if not self.is_enabled():
|
|
70
|
+
log_info("[Telemetry] Telemetry is not enabled; skipping launch ping.")
|
|
71
|
+
return
|
|
72
|
+
event = {
|
|
73
|
+
"hwid": self.get_hwid(),
|
|
74
|
+
"event_name": "app_launch",
|
|
75
|
+
"app_version": app_version,
|
|
76
|
+
"client_type": client_type,
|
|
77
|
+
"metadata": {"os": platform.system() + "-" + platform.release()},
|
|
78
|
+
}
|
|
79
|
+
log_info(f"[Telemetry] Launch event payload: {json.dumps(event, indent=2)}")
|
|
80
|
+
self.queue_event(event)
|
|
81
|
+
self.send_batch()
|
|
82
|
+
|
|
83
|
+
def purge(self):
|
|
84
|
+
self.queue = []
|
|
85
|
+
self._save_queue()
|
|
86
|
+
|
|
87
|
+
def get_queue(self):
|
|
88
|
+
return list(self.queue)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from PySide6.QtWidgets import (
|
|
2
|
+
QDialog,
|
|
3
|
+
QVBoxLayout,
|
|
4
|
+
QHBoxLayout,
|
|
5
|
+
QLabel,
|
|
6
|
+
QCheckBox,
|
|
7
|
+
QComboBox,
|
|
8
|
+
QSpinBox,
|
|
9
|
+
QPushButton,
|
|
10
|
+
)
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
|
|
13
|
+
from vector_inspector.services.settings_service import SettingsService
|
|
14
|
+
from vector_inspector.extensions import settings_panel_hook
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SettingsDialog(QDialog):
|
|
18
|
+
"""Modal settings dialog backed by SettingsService."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, settings_service: SettingsService = None, parent=None):
|
|
21
|
+
super().__init__(parent)
|
|
22
|
+
self.setWindowTitle("Preferences")
|
|
23
|
+
self.settings = settings_service or SettingsService()
|
|
24
|
+
self._init_ui()
|
|
25
|
+
self._load_values()
|
|
26
|
+
|
|
27
|
+
def _init_ui(self):
|
|
28
|
+
layout = QVBoxLayout(self)
|
|
29
|
+
|
|
30
|
+
# Breadcrumb controls are provided by pro extensions (vector-studio)
|
|
31
|
+
# via the settings_panel_hook. Core does not add breadcrumb options.
|
|
32
|
+
|
|
33
|
+
# Search defaults
|
|
34
|
+
search_layout = QHBoxLayout()
|
|
35
|
+
search_layout.addWidget(QLabel("Default results:"))
|
|
36
|
+
self.default_results = QSpinBox()
|
|
37
|
+
self.default_results.setMinimum(1)
|
|
38
|
+
self.default_results.setMaximum(1000)
|
|
39
|
+
search_layout.addWidget(self.default_results)
|
|
40
|
+
layout.addLayout(search_layout)
|
|
41
|
+
|
|
42
|
+
# Embeddings
|
|
43
|
+
self.auto_embed_checkbox = QCheckBox("Auto-generate embeddings for new text")
|
|
44
|
+
layout.addWidget(self.auto_embed_checkbox)
|
|
45
|
+
|
|
46
|
+
# Window geometry
|
|
47
|
+
self.restore_geometry_checkbox = QCheckBox("Restore window size/position on startup")
|
|
48
|
+
layout.addWidget(self.restore_geometry_checkbox)
|
|
49
|
+
|
|
50
|
+
# Buttons
|
|
51
|
+
btn_layout = QHBoxLayout()
|
|
52
|
+
self.apply_btn = QPushButton("Apply")
|
|
53
|
+
self.ok_btn = QPushButton("OK")
|
|
54
|
+
self.cancel_btn = QPushButton("Cancel")
|
|
55
|
+
self.reset_btn = QPushButton("Reset to defaults")
|
|
56
|
+
btn_layout.addWidget(self.reset_btn)
|
|
57
|
+
btn_layout.addStretch()
|
|
58
|
+
btn_layout.addWidget(self.apply_btn)
|
|
59
|
+
btn_layout.addWidget(self.ok_btn)
|
|
60
|
+
btn_layout.addWidget(self.cancel_btn)
|
|
61
|
+
# Allow external extensions to add sections before the buttons
|
|
62
|
+
try:
|
|
63
|
+
# Handlers receive (parent_layout, settings_service, dialog)
|
|
64
|
+
settings_panel_hook.trigger(layout, self.settings, self)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
layout.addLayout(btn_layout)
|
|
69
|
+
|
|
70
|
+
# Signals
|
|
71
|
+
self.apply_btn.clicked.connect(self._apply)
|
|
72
|
+
self.ok_btn.clicked.connect(self._ok)
|
|
73
|
+
self.cancel_btn.clicked.connect(self.reject)
|
|
74
|
+
self.reset_btn.clicked.connect(self._reset_defaults)
|
|
75
|
+
|
|
76
|
+
# Immediate apply on change for some controls
|
|
77
|
+
self.default_results.valueChanged.connect(lambda v: self.settings.set_default_n_results(v))
|
|
78
|
+
self.auto_embed_checkbox.stateChanged.connect(
|
|
79
|
+
lambda s: self.settings.set_auto_generate_embeddings(bool(s))
|
|
80
|
+
)
|
|
81
|
+
self.restore_geometry_checkbox.stateChanged.connect(
|
|
82
|
+
lambda s: self.settings.set_window_restore_geometry(bool(s))
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Container for programmatic sections
|
|
86
|
+
self._extra_sections = []
|
|
87
|
+
|
|
88
|
+
def add_section(self, widget_or_layout):
|
|
89
|
+
"""Programmatically add a section (widget or layout) to the dialog.
|
|
90
|
+
|
|
91
|
+
`widget_or_layout` can be a QWidget or QLayout. It will be added
|
|
92
|
+
immediately to the dialog's main layout.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
if hasattr(widget_or_layout, "setParent"):
|
|
96
|
+
# QWidget
|
|
97
|
+
self.layout().addWidget(widget_or_layout)
|
|
98
|
+
else:
|
|
99
|
+
# Assume QLayout
|
|
100
|
+
self.layout().addLayout(widget_or_layout)
|
|
101
|
+
self._extra_sections.append(widget_or_layout)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def _load_values(self):
|
|
106
|
+
# Breadcrumb controls are not present in core dialog.
|
|
107
|
+
self.default_results.setValue(self.settings.get_default_n_results())
|
|
108
|
+
self.auto_embed_checkbox.setChecked(self.settings.get_auto_generate_embeddings())
|
|
109
|
+
self.restore_geometry_checkbox.setChecked(self.settings.get_window_restore_geometry())
|
|
110
|
+
|
|
111
|
+
def _apply(self):
|
|
112
|
+
# Values are already applied on change; ensure persistence and close
|
|
113
|
+
self.settings._save_settings()
|
|
114
|
+
|
|
115
|
+
def _ok(self):
|
|
116
|
+
self._apply()
|
|
117
|
+
self.accept()
|
|
118
|
+
|
|
119
|
+
def _reset_defaults(self):
|
|
120
|
+
# Reset to recommended defaults
|
|
121
|
+
self.default_results.setValue(10)
|
|
122
|
+
self.auto_embed_checkbox.setChecked(True)
|
|
123
|
+
self.restore_geometry_checkbox.setChecked(True)
|
|
124
|
+
self._apply()
|
|
@@ -8,7 +8,7 @@ from PySide6.QtWidgets import (
|
|
|
8
8
|
QToolBar,
|
|
9
9
|
QStatusBar,
|
|
10
10
|
)
|
|
11
|
-
from PySide6.QtCore import Qt, QTimer
|
|
11
|
+
from PySide6.QtCore import Qt, QTimer, QByteArray
|
|
12
12
|
from PySide6.QtGui import QAction
|
|
13
13
|
|
|
14
14
|
from vector_inspector.core.connection_manager import ConnectionManager
|
|
@@ -58,6 +58,29 @@ class MainWindow(InspectorShell):
|
|
|
58
58
|
self._setup_statusbar()
|
|
59
59
|
self._connect_signals()
|
|
60
60
|
self._restore_session()
|
|
61
|
+
# Listen for settings changes so updates apply immediately
|
|
62
|
+
try:
|
|
63
|
+
self.settings_service.signals.setting_changed.connect(self._on_setting_changed)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
# Restore window geometry if present
|
|
67
|
+
try:
|
|
68
|
+
geom = self.settings_service.get_window_geometry()
|
|
69
|
+
if geom and self.settings_service.get_window_restore_geometry():
|
|
70
|
+
try:
|
|
71
|
+
# restoreGeometry accepts QByteArray; wrap bytes accordingly
|
|
72
|
+
if isinstance(geom, (bytes, bytearray)):
|
|
73
|
+
self.restoreGeometry(QByteArray(geom))
|
|
74
|
+
else:
|
|
75
|
+
self.restoreGeometry(geom)
|
|
76
|
+
except Exception:
|
|
77
|
+
# fallback: try passing raw bytes
|
|
78
|
+
try:
|
|
79
|
+
self.restoreGeometry(geom)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
61
84
|
# Show splash after main window is visible
|
|
62
85
|
QTimer.singleShot(0, self._maybe_show_splash)
|
|
63
86
|
|
|
@@ -121,6 +144,13 @@ class MainWindow(InspectorShell):
|
|
|
121
144
|
|
|
122
145
|
file_menu.addSeparator()
|
|
123
146
|
|
|
147
|
+
prefs_action = QAction("Preferences...", self)
|
|
148
|
+
prefs_action.setShortcut("Ctrl+,")
|
|
149
|
+
prefs_action.triggered.connect(self._show_preferences_dialog)
|
|
150
|
+
file_menu.addAction(prefs_action)
|
|
151
|
+
|
|
152
|
+
file_menu.addSeparator()
|
|
153
|
+
|
|
124
154
|
exit_action = QAction("E&xit", self)
|
|
125
155
|
exit_action.setShortcut("Ctrl+Q")
|
|
126
156
|
exit_action.triggered.connect(self.close)
|
|
@@ -249,6 +279,62 @@ class MainWindow(InspectorShell):
|
|
|
249
279
|
|
|
250
280
|
threading.Thread(target=check_updates, daemon=True).start()
|
|
251
281
|
|
|
282
|
+
def _show_preferences_dialog(self):
|
|
283
|
+
try:
|
|
284
|
+
from vector_inspector.ui.dialogs.settings_dialog import SettingsDialog
|
|
285
|
+
|
|
286
|
+
dlg = SettingsDialog(self.settings_service, self)
|
|
287
|
+
if dlg.exec() == QDialog.Accepted:
|
|
288
|
+
self._apply_settings_to_views()
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print(f"Failed to open preferences: {e}")
|
|
291
|
+
|
|
292
|
+
def _apply_settings_to_views(self):
|
|
293
|
+
"""Apply relevant settings to existing views."""
|
|
294
|
+
try:
|
|
295
|
+
# Breadcrumb visibility
|
|
296
|
+
enabled = self.settings_service.get_breadcrumb_enabled()
|
|
297
|
+
if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
|
|
298
|
+
self.search_view.breadcrumb_label.setVisible(enabled)
|
|
299
|
+
# also set elide mode
|
|
300
|
+
mode = self.settings_service.get_breadcrumb_elide_mode()
|
|
301
|
+
try:
|
|
302
|
+
self.search_view.set_elide_mode(mode)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
# Default results
|
|
307
|
+
default_n = self.settings_service.get_default_n_results()
|
|
308
|
+
if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
|
|
309
|
+
try:
|
|
310
|
+
self.search_view.n_results_spin.setValue(int(default_n))
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
def _on_setting_changed(self, key: str, value: object):
|
|
318
|
+
"""Handle granular setting change events."""
|
|
319
|
+
try:
|
|
320
|
+
if key == "breadcrumb.enabled":
|
|
321
|
+
enabled = bool(value)
|
|
322
|
+
if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
|
|
323
|
+
self.search_view.breadcrumb_label.setVisible(enabled)
|
|
324
|
+
elif key == "breadcrumb.elide_mode":
|
|
325
|
+
mode = str(value)
|
|
326
|
+
if self.search_view is not None and hasattr(self.search_view, "set_elide_mode"):
|
|
327
|
+
self.search_view.set_elide_mode(mode)
|
|
328
|
+
elif key == "search.default_n_results":
|
|
329
|
+
try:
|
|
330
|
+
n = int(value)
|
|
331
|
+
if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
|
|
332
|
+
self.search_view.n_results_spin.setValue(n)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
|
|
252
338
|
def _on_update_indicator_clicked(self, event):
|
|
253
339
|
# Show update details dialog
|
|
254
340
|
if not hasattr(self, "_latest_release"):
|
|
@@ -467,6 +553,9 @@ class MainWindow(InspectorShell):
|
|
|
467
553
|
10000,
|
|
468
554
|
)
|
|
469
555
|
|
|
556
|
+
# Apply settings to views after UI is built
|
|
557
|
+
self._apply_settings_to_views()
|
|
558
|
+
|
|
470
559
|
def _show_about(self):
|
|
471
560
|
"""Show about dialog."""
|
|
472
561
|
DialogService.show_about(self)
|
|
@@ -494,6 +583,35 @@ class MainWindow(InspectorShell):
|
|
|
494
583
|
# Refresh collections after restore
|
|
495
584
|
self._refresh_active_connection()
|
|
496
585
|
|
|
586
|
+
def show_search_results(self, collection_name: str, results: dict, context_info: str = ""):
|
|
587
|
+
"""Display search results in the Search tab.
|
|
588
|
+
|
|
589
|
+
This is an extension point that allows external code (e.g., pro features)
|
|
590
|
+
to programmatically display search results.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
collection_name: Name of the collection
|
|
594
|
+
results: Search results dictionary
|
|
595
|
+
context_info: Optional context string (e.g., "Similar to: item_123")
|
|
596
|
+
"""
|
|
597
|
+
# Switch to search tab
|
|
598
|
+
self.set_main_tab_active(InspectorTabs.SEARCH_TAB)
|
|
599
|
+
|
|
600
|
+
# Set the collection if needed
|
|
601
|
+
if self.search_view.current_collection != collection_name:
|
|
602
|
+
active = self.connection_manager.get_active_connection()
|
|
603
|
+
database_name = active.id if active else ""
|
|
604
|
+
self.search_view.set_collection(collection_name, database_name)
|
|
605
|
+
|
|
606
|
+
# Display the results
|
|
607
|
+
self.search_view.search_results = results
|
|
608
|
+
self.search_view._display_results(results)
|
|
609
|
+
|
|
610
|
+
# Update status with context if provided
|
|
611
|
+
if context_info:
|
|
612
|
+
num_results = len(results.get("ids", [[]])[0])
|
|
613
|
+
self.search_view.results_status.setText(f"{context_info} - Found {num_results} results")
|
|
614
|
+
|
|
497
615
|
def closeEvent(self, event):
|
|
498
616
|
"""Handle application close."""
|
|
499
617
|
# Clean up connection controller
|
|
@@ -508,4 +626,23 @@ class MainWindow(InspectorShell):
|
|
|
508
626
|
# Close all connections
|
|
509
627
|
self.connection_manager.close_all_connections()
|
|
510
628
|
|
|
629
|
+
# Save window geometry if enabled
|
|
630
|
+
try:
|
|
631
|
+
if self.settings_service.get_window_restore_geometry():
|
|
632
|
+
geom = self.saveGeometry()
|
|
633
|
+
# geom may be a QByteArray; convert to raw bytes
|
|
634
|
+
try:
|
|
635
|
+
if isinstance(geom, QByteArray):
|
|
636
|
+
b = bytes(geom)
|
|
637
|
+
else:
|
|
638
|
+
b = bytes(geom)
|
|
639
|
+
self.settings_service.set_window_geometry(b)
|
|
640
|
+
except Exception:
|
|
641
|
+
try:
|
|
642
|
+
self.settings_service.set_window_geometry(bytes(geom))
|
|
643
|
+
except Exception:
|
|
644
|
+
pass
|
|
645
|
+
except Exception:
|
|
646
|
+
pass
|
|
647
|
+
|
|
511
648
|
event.accept()
|
|
@@ -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.11
|
|
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
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://vector-inspector.divinedevops.com
|
|
|
8
8
|
Project-URL: Source, https://github.com/anthonypdawson/vector-inspector
|
|
9
9
|
Project-URL: Issues, https://github.com/anthonypdawson/vector-inspector/issues
|
|
10
10
|
Project-URL: Documentation, https://github.com/anthonypdawson/vector-inspector#readme
|
|
11
|
-
Requires-Python:
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
12
|
Requires-Dist: chromadb>=0.4.22
|
|
13
13
|
Requires-Dist: qdrant-client>=1.7.0
|
|
14
14
|
Requires-Dist: pyside6>=6.6.0
|
|
@@ -32,30 +32,8 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# Latest updates
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
### Major Refactor and Studio-Ready Architecture
|
|
38
|
-
- Refactored main window into modular components:
|
|
39
|
-
- InspectorShell: reusable UI shell (splitter, tabs, layout)
|
|
40
|
-
- ProviderFactory: centralized connection creation
|
|
41
|
-
- DialogService: dialog management
|
|
42
|
-
- ConnectionController: connection lifecycle and threading
|
|
43
|
-
- InspectorTabs: pluggable tab registry
|
|
44
|
-
- MainWindow now inherits from InspectorShell and is fully reusable as a widget
|
|
45
|
-
- Bootstrap logic is separated from UI logic—Studio can host Inspector as a component
|
|
46
|
-
- Tab system is now pluggable: Studio and Inspector can add, remove, or override tabs via TabDefinition
|
|
47
|
-
- All Inspector UI logic is self-contained; Studio can extend without modifying Inspector code
|
|
48
|
-
|
|
49
|
-
### Data Browser Improvements
|
|
50
|
-
- Added a checkbox: Generate embeddings on edit (default: checked)
|
|
51
|
-
- When unchecked, editing a row skips embedding regeneration
|
|
52
|
-
- Setting is persisted per user
|
|
53
|
-
|
|
54
|
-
### Developer and Architecture Notes
|
|
55
|
-
- All modules pass syntax checks and are ready for Studio integration
|
|
56
|
-
- No breaking changes for existing Inspector users
|
|
57
|
-
- Inspector is now a true UI module, not just an application
|
|
58
|
-
|
|
35
|
+
- Added optional telemetry opt-in in settings panel (File -> Preferences -> Telemetry). See docs/telemetry/model_telemetry_and_registry.md and docs/telemetry/unified_telemetry_architecture.md for details.
|
|
36
|
+
- Currently only 'launch' telemetry is sent. Model registry telemetry will be added later.
|
|
59
37
|
---
|
|
60
38
|
|
|
61
39
|
# Vector Inspector
|
|
@@ -63,7 +41,7 @@ Description-Content-Type: text/markdown
|
|
|
63
41
|
> **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.
|
|
64
42
|
|
|
65
43
|
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/ci-tests.yml)
|
|
66
|
-
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/release-and-publish.yml)
|
|
67
45
|
|
|
68
46
|
[](https://pypi.org/project/vector-inspector/)
|
|
69
47
|
[](https://pepy.tech/projects/vector-inspector)
|
|
@@ -92,6 +70,8 @@ Vector Inspector bridges the gap between vector databases and user-friendly data
|
|
|
92
70
|
|
|
93
71
|
## Key Features
|
|
94
72
|
|
|
73
|
+
> **Note:** Some features listed below may be not started or currently in progress.
|
|
74
|
+
|
|
95
75
|
### 1. **Multi-Provider Support**
|
|
96
76
|
- Connect to vector databases:
|
|
97
77
|
- ChromaDB (persistent local storage)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
vector_inspector-0.3.
|
|
2
|
-
vector_inspector-0.3.
|
|
3
|
-
vector_inspector-0.3.
|
|
4
|
-
vector_inspector/__init__.py,sha256=
|
|
1
|
+
vector_inspector-0.3.11.dist-info/METADATA,sha256=UJw5xL019__f7EIgTcIxAqQ4Wq7SjTXxZJfNLNRwVtY,10781
|
|
2
|
+
vector_inspector-0.3.11.dist-info/WHEEL,sha256=Wb0ASbVj8JvWHpOiIpPi7ucfIgJeCi__PzivviEAQFc,90
|
|
3
|
+
vector_inspector-0.3.11.dist-info/entry_points.txt,sha256=u96envMI2NFImZUJDFutiiWl7ZoHrrev9joAgtyvTxo,80
|
|
4
|
+
vector_inspector/__init__.py,sha256=gBFBsYQUOvdcns7nbI7_gtyUyzNgU6O7XqppKmCQ8f0,370
|
|
5
5
|
vector_inspector/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
|
|
6
6
|
vector_inspector/config/__init__.py,sha256=vHkVsXSUdInsfzWSOLPZzaaELa3SGenAgfpY5EYbsYA,95
|
|
7
7
|
vector_inspector/config/known_embedding_models.json,sha256=tnTWI3OvdL2z0YreL_iBzbceIXR69NDj-0tBcEV6NVM,15701
|
|
@@ -11,7 +11,7 @@ vector_inspector/core/connection_manager.py,sha256=xNmgSXqJcMC-iaOY33-Xnfxq4QjUN
|
|
|
11
11
|
vector_inspector/core/connections/__init__.py,sha256=lDZ-Qv-CbBvVcSlT8K2824zojovEIKhykHVSLARHZWs,345
|
|
12
12
|
vector_inspector/core/connections/base_connection.py,sha256=jDA1cEeNbTghqcCZYoLRpRPXIUteU5mSdpKcjvr4JQI,12236
|
|
13
13
|
vector_inspector/core/connections/chroma_connection.py,sha256=Mks17olaHIxvJAdCVB6EoQa5Tj7ScoD3_b2--Ra0Owk,21006
|
|
14
|
-
vector_inspector/core/connections/pgvector_connection.py,sha256=
|
|
14
|
+
vector_inspector/core/connections/pgvector_connection.py,sha256=7Iq6b1pWB5Rz-zva7ZvDQj098ZVMz0WUkVgarSgTpA4,44940
|
|
15
15
|
vector_inspector/core/connections/pinecone_connection.py,sha256=V6nHLB6DXfQJG8LiH1uPtWFs6otACkaNxeYrM0uY0AQ,26954
|
|
16
16
|
vector_inspector/core/connections/qdrant_connection.py,sha256=GkLmnwnswakSMRM16pmtjkR6QXBszDhvg-aKhmI9jxw,31958
|
|
17
17
|
vector_inspector/core/connections/qdrant_helpers/__init__.py,sha256=u2YNjiW4sbNtqhTNOQr4hmOFqirlNlllsyBK2gWs9eU,262
|
|
@@ -27,7 +27,9 @@ vector_inspector/core/embedding_utils.py,sha256=UCnJllDS_YPqbOPVo_kxSCUxM64C5tmc
|
|
|
27
27
|
vector_inspector/core/logging.py,sha256=HQ6_OZgZmaS3OMFOTAqc0oRbZujqo1W0w8OU4viXP1g,845
|
|
28
28
|
vector_inspector/core/model_registry.py,sha256=fdofceD3iyNpECVC7djTEAaDYgHX_7JQ3ROh5A0plpY,6269
|
|
29
29
|
vector_inspector/core/provider_factory.py,sha256=QFDpJTOBJVYAdOLlY0GxSWl87Yj_UT9ZzOm9cjVsGMU,3924
|
|
30
|
-
vector_inspector/
|
|
30
|
+
vector_inspector/extensions/__init__.py,sha256=cbN22B89XQT7uZRXBujqfbXDWm5w4rao0UHgMsUJqu0,3303
|
|
31
|
+
vector_inspector/extensions/telemetry_settings_panel.py,sha256=uYvxnjBj_sqD18TpxeZP1f1VRbYhSjQV5boVc53dx40,934
|
|
32
|
+
vector_inspector/main.py,sha256=XhoVqZJm49X5n5dGNcuTCJagU3WgCckgO0TifxAhl5A,1182
|
|
31
33
|
vector_inspector/services/__init__.py,sha256=QLgH7oybjHuEYDFNiBgmJxvSpgAzHEuBEPXa3SKJb_I,67
|
|
32
34
|
vector_inspector/services/backup_helpers.py,sha256=aX1ONFegERq6dpoNM1eJrbyE1gWCV3SuUHMyPpnxrYM,2005
|
|
33
35
|
vector_inspector/services/backup_restore_service.py,sha256=8e64C4v8v-5Sw0upC4YaJWlLFkqAkspNt0k7yZjf_zk,7625
|
|
@@ -35,7 +37,8 @@ vector_inspector/services/credential_service.py,sha256=Ui4JzivQ5YCytQYsKdzpLs5nZ
|
|
|
35
37
|
vector_inspector/services/filter_service.py,sha256=xDrMxNWsYzRcR1n0Fd-yp6Fo-4aLbVIDkhj2GKmrw5o,2370
|
|
36
38
|
vector_inspector/services/import_export_service.py,sha256=4NOfAa6ZyvMyj5cDM4xu0Wqx0pgnK3cCNBGo3E6j4LE,10200
|
|
37
39
|
vector_inspector/services/profile_service.py,sha256=AMeC6XOfI6Qumi0bKlTbqU-czMcle0rrHYK68ceK5r8,12856
|
|
38
|
-
vector_inspector/services/settings_service.py,sha256=
|
|
40
|
+
vector_inspector/services/settings_service.py,sha256=Li1JR_6Ihuta9ifo_heqbjT5e3e26kjcxR8OxPDiN58,10798
|
|
41
|
+
vector_inspector/services/telemetry_service.py,sha256=eWOmQxZjW2v_IK_HQ_uIRi4ostQyvo4ptxK3BB1cdq8,3066
|
|
39
42
|
vector_inspector/services/update_service.py,sha256=B_l0qQUe0L_rYWNYnqvXET4UNf6bCSsl2LTkINtp0GE,2771
|
|
40
43
|
vector_inspector/services/visualization_service.py,sha256=9TOK1S1u1U74wLpF5NdDiryyrjOzFnvE8kwjugf95Wk,4208
|
|
41
44
|
vector_inspector/ui/__init__.py,sha256=262ZiXO6Luk8vZnhCIoYxOtGiny0bXK-BTKjxUNBx-w,43
|
|
@@ -54,7 +57,8 @@ vector_inspector/ui/dialogs/__init__.py,sha256=xtT77L91PFfm3zHYRENHkWHJaKPm1htuU
|
|
|
54
57
|
vector_inspector/ui/dialogs/cross_db_migration.py,sha256=BaUyic8l5Ywwql2hQyxVrCXHMjGtqerNAQHDYxcbQ54,15872
|
|
55
58
|
vector_inspector/ui/dialogs/embedding_config_dialog.py,sha256=1K5LBSBXp590BvKwtHx9qgPwGREsn1mJ8cjFGSZHnMA,12926
|
|
56
59
|
vector_inspector/ui/dialogs/provider_type_dialog.py,sha256=W_FAJuvicwBUJJ7PyvKow9lc8_a5pnE3RIAsh-DVndQ,6809
|
|
57
|
-
vector_inspector/ui/
|
|
60
|
+
vector_inspector/ui/dialogs/settings_dialog.py,sha256=sMrt1uvkgu0ZPhGClKqtMYkkY2L53LyaQaWO6Cct1qc,4510
|
|
61
|
+
vector_inspector/ui/main_window.py,sha256=L8waeqVndK2YXgaf5bqHOIBbHRrcZuOkg-r_lAtrpeM,27219
|
|
58
62
|
vector_inspector/ui/main_window_shell.py,sha256=0o4KxRc4KXu-mJxni9dv74a5DzP4OIvJoLTX7BLqDoo,3425
|
|
59
63
|
vector_inspector/ui/services/__init__.py,sha256=m2DGkhYlcQQGMtNQsup5eKmhCFhOhXHi-g9Hw0GH1vE,55
|
|
60
64
|
vector_inspector/ui/services/dialog_service.py,sha256=1NHWSMvNadcmoh8tgUMSa8N7g8xYDOTaWMr1G8i9e8A,4261
|
|
@@ -63,10 +67,10 @@ vector_inspector/ui/views/__init__.py,sha256=FeMtVzSbVFBMjdwLQSQqD0FRW4ieJ4ZKXtT
|
|
|
63
67
|
vector_inspector/ui/views/collection_browser.py,sha256=oG9_YGPoVuMs-f_zSd4EcITmEU9caxvwuubsFUrNf-c,3991
|
|
64
68
|
vector_inspector/ui/views/connection_view.py,sha256=3oGbClqwpVuUD3AIT8TuM-8heDvwMYw7RowHT3b1b8o,23749
|
|
65
69
|
vector_inspector/ui/views/info_panel.py,sha256=6LwOQFmRdFOcNZo_BcZ8DZ5a5GTdGLwO2IIHqYWNBdQ,26179
|
|
66
|
-
vector_inspector/ui/views/metadata_view.py,sha256=
|
|
67
|
-
vector_inspector/ui/views/search_view.py,sha256=
|
|
70
|
+
vector_inspector/ui/views/metadata_view.py,sha256=fmofgvptCjcWrB4-M6kMN3Lbo5DYHn1VulBBrd8OYLQ,47314
|
|
71
|
+
vector_inspector/ui/views/search_view.py,sha256=xhyCf1JrqC1ZMIxTIm8nr5iC8OsbNxx1BCfabM5aRXs,17408
|
|
68
72
|
vector_inspector/ui/views/visualization_view.py,sha256=wgkSkOM-ShOHDj1GCUtKnqH87Io5vYtiOdubGV5rN44,11050
|
|
69
73
|
vector_inspector/utils/__init__.py,sha256=jhHBQC8C8bfhNlf6CAt07ejjStp_YAyleaYr2dm0Dk0,38
|
|
70
74
|
vector_inspector/utils/lazy_imports.py,sha256=2XZ3ZnwTvZ5vvrh36nJ_TUjwwkgjoAED6i6P9yctvt0,1211
|
|
71
75
|
vector_inspector/utils/version.py,sha256=2Xk9DEKlDRGEszNNiYnK7ps1i3OH56H2uZhR0_yZORs,382
|
|
72
|
-
vector_inspector-0.3.
|
|
76
|
+
vector_inspector-0.3.11.dist-info/RECORD,,
|
|
File without changes
|