vector-inspector 0.2.0__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 +3 -0
- vector_inspector/__main__.py +4 -0
- vector_inspector/core/__init__.py +1 -0
- vector_inspector/core/connections/__init__.py +7 -0
- vector_inspector/core/connections/base_connection.py +233 -0
- vector_inspector/core/connections/chroma_connection.py +384 -0
- vector_inspector/core/connections/qdrant_connection.py +723 -0
- vector_inspector/core/connections/template_connection.py +346 -0
- vector_inspector/main.py +21 -0
- vector_inspector/services/__init__.py +1 -0
- vector_inspector/services/backup_restore_service.py +286 -0
- vector_inspector/services/filter_service.py +72 -0
- vector_inspector/services/import_export_service.py +287 -0
- vector_inspector/services/settings_service.py +60 -0
- vector_inspector/services/visualization_service.py +116 -0
- vector_inspector/ui/__init__.py +1 -0
- vector_inspector/ui/components/__init__.py +1 -0
- vector_inspector/ui/components/backup_restore_dialog.py +350 -0
- vector_inspector/ui/components/filter_builder.py +370 -0
- vector_inspector/ui/components/item_dialog.py +118 -0
- vector_inspector/ui/components/loading_dialog.py +30 -0
- vector_inspector/ui/main_window.py +288 -0
- vector_inspector/ui/views/__init__.py +1 -0
- vector_inspector/ui/views/collection_browser.py +112 -0
- vector_inspector/ui/views/connection_view.py +423 -0
- vector_inspector/ui/views/metadata_view.py +555 -0
- vector_inspector/ui/views/search_view.py +268 -0
- vector_inspector/ui/views/visualization_view.py +245 -0
- vector_inspector-0.2.0.dist-info/METADATA +382 -0
- vector_inspector-0.2.0.dist-info/RECORD +32 -0
- vector_inspector-0.2.0.dist-info/WHEEL +4 -0
- vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Advanced metadata filter builder component."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox,
|
|
6
|
+
QLineEdit, QGroupBox, QScrollArea, QLabel, QFrame
|
|
7
|
+
)
|
|
8
|
+
from PySide6.QtCore import Signal, Qt
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FilterRule(QWidget):
|
|
13
|
+
"""A single filter rule widget."""
|
|
14
|
+
|
|
15
|
+
remove_requested = Signal(object) # Signal to remove this rule
|
|
16
|
+
apply_requested = Signal() # Signal to apply filters
|
|
17
|
+
|
|
18
|
+
def __init__(self, parent=None):
|
|
19
|
+
super().__init__(parent)
|
|
20
|
+
self._setup_ui()
|
|
21
|
+
|
|
22
|
+
def _setup_ui(self):
|
|
23
|
+
"""Setup the rule UI."""
|
|
24
|
+
layout = QHBoxLayout(self)
|
|
25
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
|
26
|
+
|
|
27
|
+
# Field name
|
|
28
|
+
self.field_input = QComboBox()
|
|
29
|
+
self.field_input.setEditable(True)
|
|
30
|
+
self.field_input.setPlaceholderText("field name")
|
|
31
|
+
self.field_input.setMinimumWidth(120)
|
|
32
|
+
layout.addWidget(self.field_input)
|
|
33
|
+
|
|
34
|
+
# Operator
|
|
35
|
+
self.operator_combo = QComboBox()
|
|
36
|
+
self.operator_combo.addItems([
|
|
37
|
+
"=",
|
|
38
|
+
"!=",
|
|
39
|
+
">",
|
|
40
|
+
">=",
|
|
41
|
+
"<",
|
|
42
|
+
"<=",
|
|
43
|
+
"in",
|
|
44
|
+
"not in",
|
|
45
|
+
"contains (client-side)",
|
|
46
|
+
"not contains (client-side)"
|
|
47
|
+
])
|
|
48
|
+
self.operator_combo.setMinimumWidth(150)
|
|
49
|
+
self.operator_combo.setMinimumWidth(100)
|
|
50
|
+
layout.addWidget(self.operator_combo)
|
|
51
|
+
|
|
52
|
+
# Value
|
|
53
|
+
self.value_input = QLineEdit()
|
|
54
|
+
self.value_input.setPlaceholderText("value")
|
|
55
|
+
self.value_input.setMinimumWidth(150)
|
|
56
|
+
# Apply filters on Enter key or when clicking away
|
|
57
|
+
self.value_input.returnPressed.connect(self.apply_requested.emit)
|
|
58
|
+
self.value_input.editingFinished.connect(self.apply_requested.emit)
|
|
59
|
+
layout.addWidget(self.value_input)
|
|
60
|
+
|
|
61
|
+
# Remove button
|
|
62
|
+
remove_btn = QPushButton("✕")
|
|
63
|
+
remove_btn.setMaximumWidth(30)
|
|
64
|
+
remove_btn.setStyleSheet("QPushButton { color: red; font-weight: bold; }")
|
|
65
|
+
remove_btn.clicked.connect(lambda: self.remove_requested.emit(self))
|
|
66
|
+
layout.addWidget(remove_btn)
|
|
67
|
+
|
|
68
|
+
layout.addStretch()
|
|
69
|
+
|
|
70
|
+
def set_operators(self, operators: List[Dict[str, Any]]):
|
|
71
|
+
"""Set available operators from connection provider."""
|
|
72
|
+
self.operators = operators
|
|
73
|
+
self.operator_combo.clear()
|
|
74
|
+
for op in operators:
|
|
75
|
+
name = op["name"]
|
|
76
|
+
server_side = op.get("server_side", True)
|
|
77
|
+
if not server_side:
|
|
78
|
+
name = f"{name} (client-side)"
|
|
79
|
+
self.operator_combo.addItem(name)
|
|
80
|
+
|
|
81
|
+
def set_available_fields(self, fields: List[str]):
|
|
82
|
+
"""Set the available field names in the dropdown."""
|
|
83
|
+
current_text = self.field_input.currentText()
|
|
84
|
+
self.field_input.clear()
|
|
85
|
+
self.field_input.addItems(fields)
|
|
86
|
+
# Restore the current text if it was set
|
|
87
|
+
if current_text:
|
|
88
|
+
self.field_input.setEditText(current_text)
|
|
89
|
+
|
|
90
|
+
def get_filter_dict(self) -> Optional[Dict[str, Any]]:
|
|
91
|
+
"""Get the filter as a dictionary."""
|
|
92
|
+
field = self.field_input.currentText().strip()
|
|
93
|
+
operator_display = self.operator_combo.currentText()
|
|
94
|
+
value_text = self.value_input.text().strip()
|
|
95
|
+
|
|
96
|
+
if not field or not value_text:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# Strip (client-side) suffix if present
|
|
100
|
+
operator = operator_display.replace(" (client-side)", "")
|
|
101
|
+
|
|
102
|
+
# Check if this is a client-side operator
|
|
103
|
+
is_client_side = "(client-side)" in operator_display
|
|
104
|
+
|
|
105
|
+
# Parse value (try to convert to appropriate type)
|
|
106
|
+
value = self._parse_value(value_text)
|
|
107
|
+
|
|
108
|
+
# Handle client-side operators
|
|
109
|
+
if is_client_side:
|
|
110
|
+
if operator == "contains":
|
|
111
|
+
return {"__client_side__": True, "field": field, "op": "contains", "value": value}
|
|
112
|
+
elif operator == "not contains":
|
|
113
|
+
return {"__client_side__": True, "field": field, "op": "not_contains", "value": value}
|
|
114
|
+
|
|
115
|
+
# For server-side text operators, use special syntax
|
|
116
|
+
if operator == "contains":
|
|
117
|
+
return {field: {"$contains": value}}
|
|
118
|
+
elif operator == "not contains":
|
|
119
|
+
return {field: {"$not_contains": value}}
|
|
120
|
+
|
|
121
|
+
# Map operator to database syntax (server-side)
|
|
122
|
+
if operator == "=":
|
|
123
|
+
return {field: {"$eq": value}}
|
|
124
|
+
elif operator == "!=":
|
|
125
|
+
return {field: {"$ne": value}}
|
|
126
|
+
elif operator == ">":
|
|
127
|
+
return {field: {"$gt": value}}
|
|
128
|
+
elif operator == ">=":
|
|
129
|
+
return {field: {"$gte": value}}
|
|
130
|
+
elif operator == "<":
|
|
131
|
+
return {field: {"$lt": value}}
|
|
132
|
+
elif operator == "<=":
|
|
133
|
+
return {field: {"$lte": value}}
|
|
134
|
+
elif operator == "in":
|
|
135
|
+
# Value should be comma-separated list
|
|
136
|
+
values = [self._parse_value(v.strip()) for v in value_text.split(",")]
|
|
137
|
+
return {field: {"$in": values}}
|
|
138
|
+
elif operator == "not in":
|
|
139
|
+
values = [self._parse_value(v.strip()) for v in value_text.split(",")]
|
|
140
|
+
return {field: {"$nin": values}}
|
|
141
|
+
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def _parse_value(self, value_text: str) -> Any:
|
|
145
|
+
"""Parse value text to appropriate type."""
|
|
146
|
+
# Try to parse as number
|
|
147
|
+
try:
|
|
148
|
+
if "." in value_text:
|
|
149
|
+
return float(value_text)
|
|
150
|
+
return int(value_text)
|
|
151
|
+
except ValueError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# Try to parse as boolean
|
|
155
|
+
if value_text.lower() == "true":
|
|
156
|
+
return True
|
|
157
|
+
elif value_text.lower() == "false":
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
# Return as string
|
|
161
|
+
return value_text
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class FilterBuilder(QWidget):
|
|
165
|
+
"""Advanced metadata filter builder widget."""
|
|
166
|
+
|
|
167
|
+
filter_changed = Signal() # Signal when filter changes
|
|
168
|
+
apply_filters = Signal() # Signal when user wants to apply filters
|
|
169
|
+
|
|
170
|
+
def __init__(self, parent=None):
|
|
171
|
+
super().__init__(parent)
|
|
172
|
+
self.rules: List[FilterRule] = []
|
|
173
|
+
self.available_fields: List[str] = [] # Store available field names
|
|
174
|
+
self.operators: List[Dict[str, Any]] = [] # Store operators from connection
|
|
175
|
+
self._setup_ui()
|
|
176
|
+
|
|
177
|
+
def _setup_ui(self):
|
|
178
|
+
"""Setup the filter builder UI."""
|
|
179
|
+
layout = QVBoxLayout(self)
|
|
180
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
181
|
+
|
|
182
|
+
# Header with controls
|
|
183
|
+
header_layout = QHBoxLayout()
|
|
184
|
+
|
|
185
|
+
# Logic operator (AND/OR)
|
|
186
|
+
header_layout.addWidget(QLabel("Combine rules with:"))
|
|
187
|
+
self.logic_combo = QComboBox()
|
|
188
|
+
self.logic_combo.addItems(["AND", "OR"])
|
|
189
|
+
self.logic_combo.currentTextChanged.connect(self.filter_changed.emit)
|
|
190
|
+
header_layout.addWidget(self.logic_combo)
|
|
191
|
+
|
|
192
|
+
header_layout.addStretch()
|
|
193
|
+
|
|
194
|
+
# Add rule button
|
|
195
|
+
add_btn = QPushButton("+ Add Filter Rule")
|
|
196
|
+
add_btn.clicked.connect(self._add_rule)
|
|
197
|
+
header_layout.addWidget(add_btn)
|
|
198
|
+
|
|
199
|
+
# Clear all button
|
|
200
|
+
clear_btn = QPushButton("Clear All")
|
|
201
|
+
clear_btn.clicked.connect(self._clear_all)
|
|
202
|
+
header_layout.addWidget(clear_btn)
|
|
203
|
+
|
|
204
|
+
layout.addLayout(header_layout)
|
|
205
|
+
|
|
206
|
+
# Separator
|
|
207
|
+
line = QFrame()
|
|
208
|
+
line.setFrameShape(QFrame.HLine)
|
|
209
|
+
line.setFrameShadow(QFrame.Sunken)
|
|
210
|
+
layout.addWidget(line)
|
|
211
|
+
|
|
212
|
+
# Scroll area for rules
|
|
213
|
+
scroll_area = QScrollArea()
|
|
214
|
+
scroll_area.setWidgetResizable(True)
|
|
215
|
+
scroll_area.setFrameShape(QFrame.NoFrame)
|
|
216
|
+
|
|
217
|
+
self.rules_container = QWidget()
|
|
218
|
+
self.rules_layout = QVBoxLayout(self.rules_container)
|
|
219
|
+
self.rules_layout.setAlignment(Qt.AlignTop)
|
|
220
|
+
self.rules_layout.setContentsMargins(0, 5, 0, 5)
|
|
221
|
+
|
|
222
|
+
# Placeholder label
|
|
223
|
+
self.placeholder_label = QLabel("No filters applied. Click '+ Add Filter Rule' to start.")
|
|
224
|
+
self.placeholder_label.setStyleSheet("color: gray; font-style: italic; padding: 20px;")
|
|
225
|
+
self.placeholder_label.setAlignment(Qt.AlignCenter)
|
|
226
|
+
self.rules_layout.addWidget(self.placeholder_label)
|
|
227
|
+
|
|
228
|
+
scroll_area.setWidget(self.rules_container)
|
|
229
|
+
layout.addWidget(scroll_area)
|
|
230
|
+
|
|
231
|
+
def _add_rule(self):
|
|
232
|
+
"""Add a new filter rule."""
|
|
233
|
+
rule = FilterRule()
|
|
234
|
+
rule.remove_requested.connect(self._remove_rule)
|
|
235
|
+
rule.apply_requested.connect(self.apply_filters.emit)
|
|
236
|
+
rule.field_input.editTextChanged.connect(lambda: self.filter_changed.emit())
|
|
237
|
+
rule.operator_combo.currentTextChanged.connect(lambda: self.filter_changed.emit())
|
|
238
|
+
rule.value_input.textChanged.connect(lambda: self.filter_changed.emit())
|
|
239
|
+
|
|
240
|
+
# Apply available fields if we have them
|
|
241
|
+
if self.available_fields:
|
|
242
|
+
rule.set_available_fields(self.available_fields)
|
|
243
|
+
|
|
244
|
+
# Apply operators if we have them
|
|
245
|
+
if self.operators:
|
|
246
|
+
rule.set_operators(self.operators)
|
|
247
|
+
|
|
248
|
+
self.rules.append(rule)
|
|
249
|
+
|
|
250
|
+
# Hide placeholder if this is the first rule
|
|
251
|
+
if len(self.rules) == 1:
|
|
252
|
+
self.placeholder_label.hide()
|
|
253
|
+
|
|
254
|
+
self.rules_layout.addWidget(rule)
|
|
255
|
+
self.filter_changed.emit()
|
|
256
|
+
|
|
257
|
+
def _remove_rule(self, rule: FilterRule):
|
|
258
|
+
"""Remove a filter rule."""
|
|
259
|
+
if rule in self.rules:
|
|
260
|
+
self.rules.remove(rule)
|
|
261
|
+
rule.deleteLater()
|
|
262
|
+
|
|
263
|
+
# Show placeholder if no more rules
|
|
264
|
+
if len(self.rules) == 0:
|
|
265
|
+
self.placeholder_label.show()
|
|
266
|
+
|
|
267
|
+
self.filter_changed.emit()
|
|
268
|
+
|
|
269
|
+
def _clear_all(self):
|
|
270
|
+
"""Clear all filter rules."""
|
|
271
|
+
for rule in self.rules[:]:
|
|
272
|
+
self._remove_rule(rule)
|
|
273
|
+
|
|
274
|
+
def get_filter(self) -> Optional[Dict[str, Any]]:
|
|
275
|
+
"""
|
|
276
|
+
Get the complete filter as a dictionary suitable for vector DB queries.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Filter dictionary or None if no rules
|
|
280
|
+
"""
|
|
281
|
+
if not self.rules:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
# Get all rule filters
|
|
285
|
+
rule_filters = []
|
|
286
|
+
for rule in self.rules:
|
|
287
|
+
rule_filter = rule.get_filter_dict()
|
|
288
|
+
if rule_filter:
|
|
289
|
+
rule_filters.append(rule_filter)
|
|
290
|
+
|
|
291
|
+
if not rule_filters:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
# Combine with logic operator
|
|
295
|
+
logic = self.logic_combo.currentText().lower()
|
|
296
|
+
|
|
297
|
+
if len(rule_filters) == 1:
|
|
298
|
+
return rule_filters[0]
|
|
299
|
+
|
|
300
|
+
# Combine multiple filters
|
|
301
|
+
return {f"${logic}": rule_filters}
|
|
302
|
+
|
|
303
|
+
def get_filters_split(self) -> tuple[Optional[Dict[str, Any]], list[Dict[str, Any]]]:
|
|
304
|
+
"""
|
|
305
|
+
Get filters split into server-side and client-side filters.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Tuple of (server_side_filter, client_side_filters_list)
|
|
309
|
+
"""
|
|
310
|
+
if not self.rules:
|
|
311
|
+
return None, []
|
|
312
|
+
|
|
313
|
+
server_side_filters = []
|
|
314
|
+
client_side_filters = []
|
|
315
|
+
|
|
316
|
+
for rule in self.rules:
|
|
317
|
+
rule_filter = rule.get_filter_dict()
|
|
318
|
+
if rule_filter:
|
|
319
|
+
if rule_filter.get("__client_side__"):
|
|
320
|
+
client_side_filters.append(rule_filter)
|
|
321
|
+
else:
|
|
322
|
+
server_side_filters.append(rule_filter)
|
|
323
|
+
|
|
324
|
+
# Build server-side filter
|
|
325
|
+
server_filter = None
|
|
326
|
+
if server_side_filters:
|
|
327
|
+
logic = self.logic_combo.currentText().lower()
|
|
328
|
+
if len(server_side_filters) == 1:
|
|
329
|
+
server_filter = server_side_filters[0]
|
|
330
|
+
else:
|
|
331
|
+
server_filter = {f"${logic}": server_side_filters}
|
|
332
|
+
|
|
333
|
+
return server_filter, client_side_filters
|
|
334
|
+
|
|
335
|
+
def has_filters(self) -> bool:
|
|
336
|
+
"""Check if any filters are defined."""
|
|
337
|
+
return len(self.rules) > 0
|
|
338
|
+
|
|
339
|
+
def set_available_fields(self, fields: List[str]):
|
|
340
|
+
"""Set available field names for all filter rules."""
|
|
341
|
+
self.available_fields = fields # Store for future rules
|
|
342
|
+
for rule in self.rules:
|
|
343
|
+
rule.set_available_fields(fields)
|
|
344
|
+
|
|
345
|
+
def set_operators(self, operators: List[Dict[str, Any]]):
|
|
346
|
+
"""Set available operators for all filter rules."""
|
|
347
|
+
self.operators = operators # Store for future rules
|
|
348
|
+
for rule in self.rules:
|
|
349
|
+
rule.set_operators(operators)
|
|
350
|
+
|
|
351
|
+
def get_filter_summary(self) -> str:
|
|
352
|
+
"""Get a human-readable summary of the current filters."""
|
|
353
|
+
if not self.rules:
|
|
354
|
+
return "No filters"
|
|
355
|
+
|
|
356
|
+
logic = self.logic_combo.currentText()
|
|
357
|
+
rule_summaries = []
|
|
358
|
+
|
|
359
|
+
for rule in self.rules:
|
|
360
|
+
field = rule.field_input.currentText().strip()
|
|
361
|
+
operator = rule.operator_combo.currentText()
|
|
362
|
+
value = rule.value_input.text().strip()
|
|
363
|
+
|
|
364
|
+
if field and value:
|
|
365
|
+
rule_summaries.append(f"{field} {operator} {value}")
|
|
366
|
+
|
|
367
|
+
if not rule_summaries:
|
|
368
|
+
return "No valid filters"
|
|
369
|
+
|
|
370
|
+
return f" {logic} ".join(rule_summaries)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Dialog for adding/editing items in a collection."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
|
6
|
+
QLineEdit, QTextEdit, QPushButton, QLabel, QMessageBox
|
|
7
|
+
)
|
|
8
|
+
from PySide6.QtCore import Qt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ItemDialog(QDialog):
|
|
12
|
+
"""Dialog for adding or editing a vector item."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None, item_data: Optional[Dict[str, Any]] = None):
|
|
15
|
+
super().__init__(parent)
|
|
16
|
+
self.item_data = item_data
|
|
17
|
+
self.is_edit_mode = item_data is not None
|
|
18
|
+
|
|
19
|
+
self.setWindowTitle("Edit Item" if self.is_edit_mode else "Add Item")
|
|
20
|
+
self.setMinimumWidth(500)
|
|
21
|
+
self.setMinimumHeight(400)
|
|
22
|
+
|
|
23
|
+
self._setup_ui()
|
|
24
|
+
|
|
25
|
+
if self.is_edit_mode:
|
|
26
|
+
self._populate_fields()
|
|
27
|
+
|
|
28
|
+
def _setup_ui(self):
|
|
29
|
+
"""Setup dialog UI."""
|
|
30
|
+
layout = QVBoxLayout(self)
|
|
31
|
+
|
|
32
|
+
# Form layout
|
|
33
|
+
form_layout = QFormLayout()
|
|
34
|
+
|
|
35
|
+
# ID field
|
|
36
|
+
self.id_input = QLineEdit()
|
|
37
|
+
if self.is_edit_mode:
|
|
38
|
+
self.id_input.setReadOnly(True)
|
|
39
|
+
form_layout.addRow("ID:", self.id_input)
|
|
40
|
+
|
|
41
|
+
# Document field
|
|
42
|
+
form_layout.addRow("Document:", QLabel(""))
|
|
43
|
+
self.document_input = QTextEdit()
|
|
44
|
+
self.document_input.setMaximumHeight(150)
|
|
45
|
+
form_layout.addRow(self.document_input)
|
|
46
|
+
|
|
47
|
+
# Metadata field
|
|
48
|
+
form_layout.addRow("Metadata (JSON):", QLabel(""))
|
|
49
|
+
self.metadata_input = QTextEdit()
|
|
50
|
+
self.metadata_input.setMaximumHeight(150)
|
|
51
|
+
self.metadata_input.setPlaceholderText('{"key": "value", "category": "example"}')
|
|
52
|
+
form_layout.addRow(self.metadata_input)
|
|
53
|
+
|
|
54
|
+
layout.addLayout(form_layout)
|
|
55
|
+
|
|
56
|
+
# Note about embeddings
|
|
57
|
+
note_label = QLabel(
|
|
58
|
+
"Note: Embeddings will be automatically generated from the document text."
|
|
59
|
+
)
|
|
60
|
+
note_label.setStyleSheet("color: gray; font-style: italic;")
|
|
61
|
+
note_label.setWordWrap(True)
|
|
62
|
+
layout.addWidget(note_label)
|
|
63
|
+
|
|
64
|
+
# Buttons
|
|
65
|
+
button_layout = QHBoxLayout()
|
|
66
|
+
|
|
67
|
+
save_button = QPushButton("Save" if self.is_edit_mode else "Add")
|
|
68
|
+
save_button.clicked.connect(self.accept)
|
|
69
|
+
save_button.setDefault(True)
|
|
70
|
+
|
|
71
|
+
cancel_button = QPushButton("Cancel")
|
|
72
|
+
cancel_button.clicked.connect(self.reject)
|
|
73
|
+
|
|
74
|
+
button_layout.addStretch()
|
|
75
|
+
button_layout.addWidget(save_button)
|
|
76
|
+
button_layout.addWidget(cancel_button)
|
|
77
|
+
|
|
78
|
+
layout.addLayout(button_layout)
|
|
79
|
+
|
|
80
|
+
def _populate_fields(self):
|
|
81
|
+
"""Populate fields with existing item data."""
|
|
82
|
+
if not self.item_data:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
self.id_input.setText(str(self.item_data.get("id", "")))
|
|
86
|
+
self.document_input.setPlainText(str(self.item_data.get("document", "")))
|
|
87
|
+
|
|
88
|
+
metadata = self.item_data.get("metadata", {})
|
|
89
|
+
if metadata:
|
|
90
|
+
import json
|
|
91
|
+
self.metadata_input.setPlainText(json.dumps(metadata, indent=2))
|
|
92
|
+
|
|
93
|
+
def get_item_data(self) -> Dict[str, Any]:
|
|
94
|
+
"""Get item data from dialog fields."""
|
|
95
|
+
import json
|
|
96
|
+
|
|
97
|
+
item_id = self.id_input.text().strip()
|
|
98
|
+
document = self.document_input.toPlainText().strip()
|
|
99
|
+
|
|
100
|
+
# Parse metadata
|
|
101
|
+
metadata = {}
|
|
102
|
+
metadata_text = self.metadata_input.toPlainText().strip()
|
|
103
|
+
if metadata_text:
|
|
104
|
+
try:
|
|
105
|
+
metadata = json.loads(metadata_text)
|
|
106
|
+
except json.JSONDecodeError:
|
|
107
|
+
QMessageBox.warning(
|
|
108
|
+
self,
|
|
109
|
+
"Invalid Metadata",
|
|
110
|
+
"Metadata must be valid JSON format."
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"id": item_id,
|
|
116
|
+
"document": document,
|
|
117
|
+
"metadata": metadata
|
|
118
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from PySide6.QtWidgets import QProgressDialog, QApplication
|
|
2
|
+
from PySide6.QtCore import Qt
|
|
3
|
+
|
|
4
|
+
class LoadingDialog(QProgressDialog):
|
|
5
|
+
def __init__(self, message="Loading...", parent=None):
|
|
6
|
+
super().__init__(message, None, 0, 0, parent)
|
|
7
|
+
self.setWindowTitle("Please Wait")
|
|
8
|
+
self.setWindowModality(Qt.ApplicationModal)
|
|
9
|
+
self.setCancelButton(None)
|
|
10
|
+
self.setMinimumDuration(0)
|
|
11
|
+
self.setAutoClose(False)
|
|
12
|
+
self.setAutoReset(False)
|
|
13
|
+
self.setValue(0)
|
|
14
|
+
self.setMinimumWidth(300)
|
|
15
|
+
self.reset() # Hide dialog by default until show_loading() is called
|
|
16
|
+
|
|
17
|
+
def show_loading(self, message=None):
|
|
18
|
+
if message:
|
|
19
|
+
self.setLabelText(message)
|
|
20
|
+
self.setValue(0)
|
|
21
|
+
self.show()
|
|
22
|
+
# Force the dialog to render by processing events multiple times
|
|
23
|
+
QApplication.processEvents()
|
|
24
|
+
self.repaint()
|
|
25
|
+
QApplication.processEvents()
|
|
26
|
+
|
|
27
|
+
def hide_loading(self):
|
|
28
|
+
self.reset()
|
|
29
|
+
self.hide()
|
|
30
|
+
self.close()
|