supervertaler 1.9.116__py3-none-any.whl → 1.9.131__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.
@@ -18,7 +18,7 @@ from PyQt6.QtWidgets import (
18
18
  QTextEdit, QPlainTextEdit, QSplitter, QGroupBox, QMessageBox, QFileDialog,
19
19
  QInputDialog, QLineEdit, QFrame, QMenu, QCheckBox, QSizePolicy, QScrollArea, QTabWidget,
20
20
  QListWidget, QListWidgetItem, QStyledItemDelegate, QStyleOptionViewItem, QApplication, QDialog,
21
- QAbstractItemView
21
+ QAbstractItemView, QTableWidget, QTableWidgetItem, QHeaderView
22
22
  )
23
23
  from PyQt6.QtCore import Qt, QSettings, pyqtSignal, QThread, QSize, QRect, QRectF
24
24
  from PyQt6.QtGui import QFont, QColor, QAction, QIcon, QPainter, QPen, QBrush, QPainterPath, QLinearGradient
@@ -31,6 +31,89 @@ from modules.ai_file_viewer_dialog import FileViewerDialog, FileRemoveConfirmDia
31
31
  from modules.ai_actions import AIActionSystem
32
32
 
33
33
 
34
+ class CheckmarkCheckBox(QCheckBox):
35
+ """Custom checkbox with green background and white checkmark when checked"""
36
+
37
+ def __init__(self, text="", parent=None):
38
+ super().__init__(text, parent)
39
+ self.setCheckable(True)
40
+ self.setEnabled(True)
41
+ self.setStyleSheet("""
42
+ QCheckBox {
43
+ font-size: 9pt;
44
+ spacing: 6px;
45
+ }
46
+ QCheckBox::indicator {
47
+ width: 16px;
48
+ height: 16px;
49
+ border: 2px solid #999;
50
+ border-radius: 3px;
51
+ background-color: white;
52
+ }
53
+ QCheckBox::indicator:checked {
54
+ background-color: #4CAF50;
55
+ border-color: #4CAF50;
56
+ }
57
+ QCheckBox::indicator:hover {
58
+ border-color: #666;
59
+ }
60
+ QCheckBox::indicator:checked:hover {
61
+ background-color: #45a049;
62
+ border-color: #45a049;
63
+ }
64
+ """)
65
+
66
+ def paintEvent(self, event):
67
+ """Override paint event to draw white checkmark when checked"""
68
+ super().paintEvent(event)
69
+
70
+ if self.isChecked():
71
+ # Get the indicator rectangle using QStyle
72
+ from PyQt6.QtWidgets import QStyleOptionButton
73
+ from PyQt6.QtCore import QPointF
74
+
75
+ opt = QStyleOptionButton()
76
+ self.initStyleOption(opt)
77
+ indicator_rect = self.style().subElementRect(
78
+ self.style().SubElement.SE_CheckBoxIndicator,
79
+ opt,
80
+ self
81
+ )
82
+
83
+ if indicator_rect.isValid():
84
+ # Draw white checkmark
85
+ painter = QPainter(self)
86
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
87
+ pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
88
+ painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
89
+ painter.setBrush(QColor(255, 255, 255))
90
+
91
+ # Draw checkmark (✓ shape) - coordinates relative to indicator
92
+ x = indicator_rect.x()
93
+ y = indicator_rect.y()
94
+ w = indicator_rect.width()
95
+ h = indicator_rect.height()
96
+
97
+ # Add padding (15% on all sides)
98
+ padding = min(w, h) * 0.15
99
+ x += padding
100
+ y += padding
101
+ w -= padding * 2
102
+ h -= padding * 2
103
+
104
+ # Checkmark path
105
+ check_x1 = x + w * 0.10
106
+ check_y1 = y + h * 0.50
107
+ check_x2 = x + w * 0.35
108
+ check_y2 = y + h * 0.70
109
+ check_x3 = x + w * 0.90
110
+ check_y3 = y + h * 0.25
111
+
112
+ # Draw two lines forming the checkmark
113
+ painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
114
+ painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
115
+
116
+
34
117
  class PromptLibraryTreeWidget(QTreeWidget):
35
118
  """Tree widget that supports drag-and-drop moves for prompt files."""
36
119
 
@@ -563,6 +646,10 @@ class UnifiedPromptManagerQt:
563
646
  assistant_tab = self._create_ai_assistant_tab()
564
647
  self.sub_tabs.addTab(assistant_tab, "✨ AI Assistant")
565
648
 
649
+ # Tab 3: Placeholders Reference
650
+ placeholders_tab = self._create_placeholders_tab()
651
+ self.sub_tabs.addTab(placeholders_tab, "📝 Placeholders")
652
+
566
653
  # Connect tab change signal to update context
567
654
  self.sub_tabs.currentChanged.connect(self._on_tab_changed)
568
655
 
@@ -764,6 +851,153 @@ class UnifiedPromptManagerQt:
764
851
 
765
852
  return tab
766
853
 
854
+ def _create_placeholders_tab(self) -> QWidget:
855
+ """Create the Placeholders Reference sub-tab"""
856
+ tab = QWidget()
857
+ layout = QVBoxLayout(tab)
858
+ layout.setContentsMargins(10, 10, 10, 10)
859
+ layout.setSpacing(5)
860
+
861
+ # Header (matches standard tool style: Superbench, AutoFingers, TMX Editor)
862
+ header = QLabel("📝 Available Placeholders")
863
+ header.setStyleSheet("font-size: 16pt; font-weight: bold; color: #1976D2;")
864
+ layout.addWidget(header, 0)
865
+
866
+ # Description box (matches standard tool style)
867
+ description = QLabel(
868
+ "Use these placeholders in your prompts. They will be replaced with actual values when the prompt runs."
869
+ )
870
+ description.setWordWrap(True)
871
+ description.setStyleSheet("color: #666; padding: 5px; background-color: #E3F2FD; border-radius: 3px;")
872
+ layout.addWidget(description, 0)
873
+
874
+ # Horizontal splitter for table and tips
875
+ splitter = QSplitter(Qt.Orientation.Horizontal)
876
+ splitter.setHandleWidth(3)
877
+
878
+ # Left: Table with placeholders
879
+ table = QTableWidget()
880
+ table.setColumnCount(3)
881
+ table.setHorizontalHeaderLabels(["Placeholder", "Description", "Example"])
882
+ table.horizontalHeader().setStretchLastSection(True)
883
+ table.verticalHeader().setVisible(False)
884
+ table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
885
+ table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
886
+ table.setAlternatingRowColors(True)
887
+
888
+ # Placeholder data
889
+ placeholders = [
890
+ (
891
+ "{{SELECTION}}",
892
+ "Currently selected text in the grid (source or target cell)",
893
+ "If you select 'translation memory' in the grid, this will contain that text"
894
+ ),
895
+ (
896
+ "{{SOURCE_TEXT}}",
897
+ "Full text of the current source segment",
898
+ "The complete source sentence/paragraph from the active segment"
899
+ ),
900
+ (
901
+ "{{SOURCE_LANGUAGE}}",
902
+ "Project's source language",
903
+ "Dutch, English, German, French, etc."
904
+ ),
905
+ (
906
+ "{{TARGET_LANGUAGE}}",
907
+ "Project's target language",
908
+ "English, Spanish, Portuguese, etc."
909
+ ),
910
+ (
911
+ "{{SOURCE+TARGET_CONTEXT}}",
912
+ "Project segments with BOTH source and target text. Use for proofreading prompts.",
913
+ "[1] Source text\\n → Target text\\n\\n[2] Source text\\n → Target text\\n\\n..."
914
+ ),
915
+ (
916
+ "{{SOURCE_CONTEXT}}",
917
+ "Project segments with SOURCE ONLY. Use for translation/terminology questions.",
918
+ "[1] Source text\\n\\n[2] Source text\\n\\n[3] Source text\\n\\n..."
919
+ ),
920
+ (
921
+ "{{TARGET_CONTEXT}}",
922
+ "Project segments with TARGET ONLY. Use for consistency/style analysis.",
923
+ "[1] Target text\\n\\n[2] Target text\\n\\n[3] Target text\\n\\n..."
924
+ )
925
+ ]
926
+
927
+ table.setRowCount(len(placeholders))
928
+ for row, (placeholder, description, example) in enumerate(placeholders):
929
+ # Placeholder column (monospace, bold)
930
+ item_placeholder = QTableWidgetItem(placeholder)
931
+ item_placeholder.setFont(QFont("Courier New", 10, QFont.Weight.Bold))
932
+ table.setItem(row, 0, item_placeholder)
933
+
934
+ # Description column
935
+ item_desc = QTableWidgetItem(description)
936
+ item_desc.setToolTip(description)
937
+ table.setItem(row, 1, item_desc)
938
+
939
+ # Example column (monospace, italic)
940
+ item_example = QTableWidgetItem(example)
941
+ item_example.setFont(QFont("Courier New", 9))
942
+ item_example.setToolTip(example)
943
+ table.setItem(row, 2, item_example)
944
+
945
+ # Set column widths
946
+ table.setColumnWidth(0, 200)
947
+ table.setColumnWidth(1, 300)
948
+ header = table.horizontalHeader()
949
+ header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
950
+ header.setSectionResizeMode(1, QHeaderView.ResizeMode.Interactive)
951
+ header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
952
+
953
+ # Adjust row heights for readability
954
+ for row in range(table.rowCount()):
955
+ table.setRowHeight(row, 60)
956
+
957
+ splitter.addWidget(table)
958
+
959
+ # Right: Usage tips panel
960
+ tips_panel = QWidget()
961
+ tips_layout = QVBoxLayout(tips_panel)
962
+ tips_layout.setContentsMargins(10, 0, 0, 0)
963
+
964
+ tips_header = QLabel("💡 Usage Tips")
965
+ tips_header.setStyleSheet("font-weight: bold; font-size: 11pt; color: #2196F3; margin-bottom: 8px;")
966
+ tips_layout.addWidget(tips_header)
967
+
968
+ tips_intro = QLabel(
969
+ "Use these placeholders in your prompts. They will be replaced with actual values when the prompt runs."
970
+ )
971
+ tips_intro.setWordWrap(True)
972
+ tips_intro.setStyleSheet("color: #666; margin-bottom: 15px; font-style: italic;")
973
+ tips_layout.addWidget(tips_intro)
974
+
975
+ tips_text = QLabel(
976
+ "• Placeholders are case-sensitive (use UPPERCASE)\n\n"
977
+ "• Surround placeholders with double curly braces: {{ }}\n\n"
978
+ "• You can combine multiple placeholders in one prompt\n\n"
979
+ "• Use {{DOCUMENT_CONTEXT}} for context-aware translations\n\n"
980
+ "• Configure {{DOCUMENT_CONTEXT}} percentage in Settings → AI Settings"
981
+ )
982
+ tips_text.setWordWrap(True)
983
+ tips_text.setStyleSheet("color: #666; line-height: 1.6;")
984
+ tips_layout.addWidget(tips_text)
985
+
986
+ tips_layout.addStretch()
987
+
988
+ tips_panel.setMinimumWidth(280)
989
+ tips_panel.setMaximumWidth(400)
990
+ splitter.addWidget(tips_panel)
991
+
992
+ # Set splitter proportions (75% table, 25% tips)
993
+ splitter.setSizes([750, 250])
994
+ splitter.setStretchFactor(0, 1) # Table expands
995
+ splitter.setStretchFactor(1, 0) # Tips panel fixed-ish
996
+
997
+ layout.addWidget(splitter, 1) # 1 = stretch to fill all available space
998
+
999
+ return tab
1000
+
767
1001
  def _create_context_sidebar(self) -> QWidget:
768
1002
  """Create context sidebar showing available resources"""
769
1003
  panel = QWidget()
@@ -1470,10 +1704,10 @@ class UnifiedPromptManagerQt:
1470
1704
  self.editor_quickmenu_label_input.setPlaceholderText("Label shown in QuickMenu")
1471
1705
  quickmenu_layout.addWidget(self.editor_quickmenu_label_input, 2)
1472
1706
 
1473
- self.editor_quickmenu_in_grid_cb = QCheckBox("Show in Grid right-click QuickMenu")
1707
+ self.editor_quickmenu_in_grid_cb = CheckmarkCheckBox("Show in Grid right-click QuickMenu")
1474
1708
  quickmenu_layout.addWidget(self.editor_quickmenu_in_grid_cb, 2)
1475
1709
 
1476
- self.editor_quickmenu_in_quickmenu_cb = QCheckBox("Show in QuickMenu")
1710
+ self.editor_quickmenu_in_quickmenu_cb = CheckmarkCheckBox("Show in Supervertaler QuickMenu")
1477
1711
  quickmenu_layout.addWidget(self.editor_quickmenu_in_quickmenu_cb, 1)
1478
1712
 
1479
1713
  layout.addLayout(quickmenu_layout)
@@ -1950,7 +2184,7 @@ class UnifiedPromptManagerQt:
1950
2184
  indicators = []
1951
2185
  if prompt_data.get('favorite'):
1952
2186
  indicators.append("⭐")
1953
- if prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)):
2187
+ if prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)):
1954
2188
  indicators.append("⚡")
1955
2189
  if prompt_data.get('quickmenu_grid', False):
1956
2190
  indicators.append("🖱️")
@@ -2018,7 +2252,7 @@ class UnifiedPromptManagerQt:
2018
2252
  action_fav.triggered.connect(lambda: self._toggle_favorite(path))
2019
2253
 
2020
2254
  # Toggle QuickMenu (legacy: quick_run)
2021
- if prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)):
2255
+ if prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)):
2022
2256
  action_qr = menu.addAction("⚡ Remove from QuickMenu")
2023
2257
  else:
2024
2258
  action_qr = menu.addAction("⚡ Add to QuickMenu")
@@ -2070,7 +2304,7 @@ class UnifiedPromptManagerQt:
2070
2304
  if hasattr(self, 'editor_quickmenu_in_grid_cb'):
2071
2305
  self.editor_quickmenu_in_grid_cb.setChecked(bool(prompt_data.get('quickmenu_grid', False)))
2072
2306
  if hasattr(self, 'editor_quickmenu_in_quickmenu_cb'):
2073
- self.editor_quickmenu_in_quickmenu_cb.setChecked(bool(prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False))))
2307
+ self.editor_quickmenu_in_quickmenu_cb.setChecked(bool(prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False))))
2074
2308
  self.editor_content.setPlainText(prompt_data.get('content', ''))
2075
2309
 
2076
2310
  # Store current path for saving
@@ -2083,119 +2317,127 @@ class UnifiedPromptManagerQt:
2083
2317
 
2084
2318
  def _save_current_prompt(self):
2085
2319
  """Save currently edited prompt"""
2086
- name = self.editor_name_input.text().strip()
2087
- description = self.editor_desc_input.text().strip()
2088
- content = self.editor_content.toPlainText().strip()
2089
-
2090
- # Name field now represents the complete filename with extension
2091
- # No stripping needed - user sees and edits the full filename
2092
-
2093
- quickmenu_label = ''
2094
- quickmenu_grid = False
2095
- quickmenu_quickmenu = False
2096
- if hasattr(self, 'editor_quickmenu_label_input'):
2097
- quickmenu_label = self.editor_quickmenu_label_input.text().strip()
2098
- if hasattr(self, 'editor_quickmenu_in_grid_cb'):
2099
- quickmenu_grid = bool(self.editor_quickmenu_in_grid_cb.isChecked())
2100
- if hasattr(self, 'editor_quickmenu_in_quickmenu_cb'):
2101
- quickmenu_quickmenu = bool(self.editor_quickmenu_in_quickmenu_cb.isChecked())
2102
-
2103
- if not name or not content:
2104
- QMessageBox.warning(self.main_widget, "Error", "Name and content are required")
2105
- return
2106
-
2107
- # Check if this is a new prompt or editing existing
2108
- if hasattr(self, 'editor_current_path') and self.editor_current_path:
2109
- path = self.editor_current_path
2110
-
2111
- # Handle external prompts (save back to external file)
2112
- if path.startswith("[EXTERNAL] "):
2113
- external_file_path = path[11:] # Remove "[EXTERNAL] " prefix
2114
- self._save_external_prompt(external_file_path, name, description, content)
2115
- return
2320
+ try:
2321
+ name = self.editor_name_input.text().strip()
2322
+ description = self.editor_desc_input.text().strip()
2323
+ content = self.editor_content.toPlainText().strip()
2116
2324
 
2117
- # Editing existing library prompt
2118
- if path not in self.library.prompts:
2119
- QMessageBox.warning(self.main_widget, "Error", "Prompt no longer exists")
2325
+ # Name field now represents the complete filename with extension
2326
+ # No stripping needed - user sees and edits the full filename
2327
+
2328
+ quickmenu_label = ''
2329
+ quickmenu_grid = False
2330
+ sv_quickmenu = False
2331
+ if hasattr(self, 'editor_quickmenu_label_input'):
2332
+ quickmenu_label = self.editor_quickmenu_label_input.text().strip()
2333
+ if hasattr(self, 'editor_quickmenu_in_grid_cb'):
2334
+ quickmenu_grid = bool(self.editor_quickmenu_in_grid_cb.isChecked())
2335
+ if hasattr(self, 'editor_quickmenu_in_quickmenu_cb'):
2336
+ sv_quickmenu = bool(self.editor_quickmenu_in_quickmenu_cb.isChecked())
2337
+
2338
+ if not name or not content:
2339
+ QMessageBox.warning(self.main_widget, "Error", "Name and content are required")
2120
2340
  return
2121
2341
 
2122
- prompt_data = self.library.prompts[path].copy()
2123
- old_filename = Path(path).name
2124
-
2125
- # Extract name without extension for metadata
2126
- name_without_ext = Path(name).stem
2127
-
2128
- prompt_data['name'] = name_without_ext
2129
- prompt_data['description'] = description
2130
- prompt_data['content'] = content
2131
- prompt_data['quickmenu_label'] = quickmenu_label or name_without_ext
2132
- prompt_data['quickmenu_grid'] = quickmenu_grid
2133
- prompt_data['quickmenu_quickmenu'] = quickmenu_quickmenu
2134
- # Keep legacy field in sync
2135
- prompt_data['quick_run'] = quickmenu_quickmenu
2136
-
2137
- # Check if filename changed - need to rename file
2138
- if old_filename != name:
2139
- from pathlib import Path
2140
- old_path = Path(path)
2141
- folder = str(old_path.parent) if old_path.parent != Path('.') else ''
2142
- new_path = f"{folder}/{name}" if folder else name
2342
+ # Check if this is a new prompt or editing existing
2343
+ if hasattr(self, 'editor_current_path') and self.editor_current_path:
2344
+ path = self.editor_current_path
2143
2345
 
2144
- # Delete old file and save to new location
2145
- if self.library.delete_prompt(path):
2146
- if self.library.save_prompt(new_path, prompt_data):
2147
- self.library.load_all_prompts()
2148
- self._refresh_tree()
2149
- self._select_and_reveal_prompt(new_path)
2150
- self.editor_current_path = new_path # Update to new path
2151
- QMessageBox.information(self.main_widget, "Saved", f"Prompt renamed to '{name}' successfully!")
2152
- self.log_message(f" Renamed prompt: {old_filename} → {name}")
2346
+ # Handle external prompts (save back to external file)
2347
+ if path.startswith("[EXTERNAL] "):
2348
+ external_file_path = path[11:] # Remove "[EXTERNAL] " prefix
2349
+ self._save_external_prompt(external_file_path, name, description, content)
2350
+ return
2351
+
2352
+ # Editing existing library prompt
2353
+ if path not in self.library.prompts:
2354
+ QMessageBox.warning(self.main_widget, "Error", "Prompt no longer exists")
2355
+ return
2356
+
2357
+ prompt_data = self.library.prompts[path].copy()
2358
+ old_filename = Path(path).name
2359
+
2360
+ # Extract name without extension for metadata
2361
+ name_without_ext = Path(name).stem
2362
+
2363
+ prompt_data['name'] = name_without_ext
2364
+ prompt_data['description'] = description
2365
+ prompt_data['content'] = content
2366
+ prompt_data['quickmenu_label'] = quickmenu_label or name_without_ext
2367
+ prompt_data['quickmenu_grid'] = quickmenu_grid
2368
+ prompt_data['sv_quickmenu'] = sv_quickmenu
2369
+ # Keep legacy field in sync
2370
+ prompt_data['quick_run'] = sv_quickmenu
2371
+
2372
+ # Check if filename changed - need to rename file
2373
+ if old_filename != name:
2374
+ old_path = Path(path)
2375
+ folder = str(old_path.parent) if old_path.parent != Path('.') else ''
2376
+ new_path = f"{folder}/{name}" if folder else name
2377
+
2378
+ # Delete old file and save to new location
2379
+ if self.library.delete_prompt(path):
2380
+ if self.library.save_prompt(new_path, prompt_data):
2381
+ self.library.load_all_prompts()
2382
+ self._refresh_tree()
2383
+ self._select_and_reveal_prompt(new_path)
2384
+ self.editor_current_path = new_path # Update to new path
2385
+ QMessageBox.information(self.main_widget, "Saved", f"Prompt renamed to '{name}' successfully!")
2386
+ self.log_message(f"✓ Renamed prompt: {old_filename} → {name}")
2387
+ else:
2388
+ QMessageBox.warning(self.main_widget, "Error", "Failed to rename prompt")
2153
2389
  else:
2154
- QMessageBox.warning(self.main_widget, "Error", "Failed to rename prompt")
2390
+ QMessageBox.warning(self.main_widget, "Error", "Failed to delete old prompt file")
2155
2391
  else:
2156
- QMessageBox.warning(self.main_widget, "Error", "Failed to delete old prompt file")
2392
+ # Name unchanged, just update in place
2393
+ if self.library.save_prompt(path, prompt_data):
2394
+ QMessageBox.information(self.main_widget, "Saved", "Prompt updated successfully!")
2395
+ self._refresh_tree()
2396
+ else:
2397
+ QMessageBox.warning(self.main_widget, "Error", "Failed to save prompt")
2157
2398
  else:
2158
- # Name unchanged, just update in place
2159
- if self.library.save_prompt(path, prompt_data):
2160
- QMessageBox.information(self.main_widget, "Saved", "Prompt updated successfully!")
2399
+ # Creating new prompt
2400
+ folder = getattr(self, 'editor_target_folder', 'Project Prompts')
2401
+
2402
+ # Create new prompt data
2403
+ from datetime import datetime
2404
+ prompt_data = {
2405
+ 'name': name,
2406
+ 'description': description,
2407
+ 'content': content,
2408
+ 'domain': '',
2409
+ 'version': '1.0',
2410
+ 'task_type': 'Translation',
2411
+ 'favorite': False,
2412
+ # QuickMenu
2413
+ 'quickmenu_label': quickmenu_label or name,
2414
+ 'quickmenu_grid': quickmenu_grid,
2415
+ 'sv_quickmenu': sv_quickmenu,
2416
+ # Legacy
2417
+ 'quick_run': sv_quickmenu,
2418
+ 'folder': folder,
2419
+ 'tags': [],
2420
+ 'created': datetime.now().strftime('%Y-%m-%d'),
2421
+ 'modified': datetime.now().strftime('%Y-%m-%d')
2422
+ }
2423
+
2424
+ # Create the prompt file (save_prompt creates new file if it doesn't exist)
2425
+ relative_path = f"{folder}/{name}.svprompt"
2426
+ if self.library.save_prompt(relative_path, prompt_data):
2427
+ QMessageBox.information(self.main_widget, "Created", f"Prompt '{name}' created successfully!")
2428
+ self.library.load_all_prompts() # Reload to get new prompt in memory
2161
2429
  self._refresh_tree()
2430
+ self.editor_current_path = relative_path # Now editing this prompt
2162
2431
  else:
2163
- QMessageBox.warning(self.main_widget, "Error", "Failed to save prompt")
2164
- else:
2165
- # Creating new prompt
2166
- folder = getattr(self, 'editor_target_folder', 'Project Prompts')
2167
-
2168
- # Create new prompt data
2169
- from datetime import datetime
2170
- prompt_data = {
2171
- 'name': name,
2172
- 'description': description,
2173
- 'content': content,
2174
- 'domain': '',
2175
- 'version': '1.0',
2176
- 'task_type': 'Translation',
2177
- 'favorite': False,
2178
- # QuickMenu
2179
- 'quickmenu_label': quickmenu_label or name,
2180
- 'quickmenu_grid': quickmenu_grid,
2181
- 'quickmenu_quickmenu': quickmenu_quickmenu,
2182
- # Legacy
2183
- 'quick_run': quickmenu_quickmenu,
2184
- 'folder': folder,
2185
- 'tags': [],
2186
- 'created': datetime.now().strftime('%Y-%m-%d'),
2187
- 'modified': datetime.now().strftime('%Y-%m-%d')
2188
- }
2189
-
2190
- # Create the prompt file (save_prompt creates new file if it doesn't exist)
2191
- relative_path = f"{folder}/{name}.svprompt"
2192
- if self.library.save_prompt(relative_path, prompt_data):
2193
- QMessageBox.information(self.main_widget, "Created", f"Prompt '{name}' created successfully!")
2194
- self.library.load_all_prompts() # Reload to get new prompt in memory
2195
- self._refresh_tree()
2196
- self.editor_current_path = relative_path # Now editing this prompt
2197
- else:
2198
- QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
2432
+ QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
2433
+
2434
+ except Exception as e:
2435
+ import traceback
2436
+ error_msg = f"Prompt save error: {str(e)}\n{traceback.format_exc()}"
2437
+ print(f"[ERROR] {error_msg}")
2438
+ self.log_message(f"❌ Prompt save error: {str(e)}")
2439
+ QMessageBox.critical(self.main_widget, "Save Error", f"Failed to save prompt:\n\n{str(e)}")
2440
+ return
2199
2441
 
2200
2442
  def _save_external_prompt(self, file_path: str, name: str, description: str, content: str):
2201
2443
  """Save changes to an external prompt file"""
@@ -2421,7 +2663,7 @@ class UnifiedPromptManagerQt:
2421
2663
  'favorite': False,
2422
2664
  'quickmenu_label': name_without_ext,
2423
2665
  'quickmenu_grid': False,
2424
- 'quickmenu_quickmenu': False,
2666
+ 'sv_quickmenu': False,
2425
2667
  'quick_run': False,
2426
2668
  'folder': folder_path,
2427
2669
  'tags': [],
@@ -2437,6 +2679,7 @@ class UnifiedPromptManagerQt:
2437
2679
  self._refresh_tree()
2438
2680
  self._select_and_reveal_prompt(relative_path)
2439
2681
  self._load_prompt_in_editor(relative_path)
2682
+ self.btn_save_prompt.setEnabled(True) # Ensure Save button is enabled for new prompt
2440
2683
  self.log_message(f"✓ Created new prompt '{name}' in folder: {folder_path}")
2441
2684
  else:
2442
2685
  QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
@@ -2469,7 +2712,7 @@ class UnifiedPromptManagerQt:
2469
2712
  src_data['favorite'] = False
2470
2713
  src_data['quick_run'] = False
2471
2714
  src_data['quickmenu_grid'] = False
2472
- src_data['quickmenu_quickmenu'] = False
2715
+ src_data['sv_quickmenu'] = False
2473
2716
  src_data['folder'] = folder
2474
2717
  src_data['created'] = datetime.now().strftime('%Y-%m-%d')
2475
2718
  src_data['modified'] = datetime.now().strftime('%Y-%m-%d')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supervertaler
3
- Version: 1.9.116
3
+ Version: 1.9.131
4
4
  Summary: Professional AI-powered translation workbench with multi-LLM support, glossary system, TM, spellcheck, voice commands, and PyQt6 interface. Batteries included (core).
5
5
  Home-page: https://supervertaler.com
6
6
  Author: Michael Beijer
@@ -71,7 +71,7 @@ Dynamic: home-page
71
71
  Dynamic: license-file
72
72
  Dynamic: requires-python
73
73
 
74
- # 🚀 Supervertaler v1.9.116
74
+ # 🚀 Supervertaler v1.9.128
75
75
 
76
76
  [![PyPI version](https://badge.fury.io/py/supervertaler.svg)](https://pypi.org/project/Supervertaler/)
77
77
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
@@ -79,7 +79,41 @@ Dynamic: requires-python
79
79
 
80
80
  AI-enhanced CAT tool with multi-LLM support (GPT-4, Claude, Gemini, Ollama), innovative Superlookup concordance system offering access to multiple terminology sources (TMs, glossaries, web resources, etc.), and seamless CAT tool integration (memoQ, Trados, CafeTran, Phrase).
81
81
 
82
- **Current Version:** v1.9.116 (January 19, 2026)
82
+ **Current Version:** v1.9.131 (January 20, 2026)
83
+
84
+ ### ENHANCED in v1.9.128 - 📝 Placeholders Tab Layout Optimization
85
+
86
+ **Better Use of Space:** The Placeholders reference tab in the Prompt Manager now uses a compact sidebar layout for Usage Tips, giving the placeholders table much more vertical space. Matches the clean, efficient layout style of other tools like AutoFingers and TMX Editor.
87
+
88
+ ### FIXED in v1.9.125 - 🐛 Prompt Save Crash
89
+
90
+ **Critical Fix:** Wrapped prompt save logic in comprehensive error handling to prevent silent crashes. Now shows detailed error messages if save fails instead of crashing the app.
91
+
92
+ ### ADDED in v1.9.124 - 📄 QuickMenu Document Context
93
+
94
+ **Context-Aware AI Suggestions:** QuickMenu prompts can now access the full document context! Use the new `{{DOCUMENT_CONTEXT}}` placeholder to give the AI access to your project's source segments. Configure what percentage of segments to include (0-100%, default 50%) in Settings → AI Settings. Perfect for: "Suggest the best translation of '{{SELECTION}}' within the context of this project."
95
+
96
+ ### FIXED in v1.9.123 - 🤖 QuickMenu Generic AI Support
97
+
98
+ **QuickMenu Now Works for Any Task:** Fixed bug where QuickMenu prompts were forced into translation mode. Now supports any AI task: explain terms, define concepts, suggest multiple options, analyze tone, etc.
99
+
100
+ ### ENHANCED in v1.9.122 - ⌨️ Ctrl+N for Quick Notes
101
+
102
+ **Faster Note-Taking:** Ctrl+N now focuses the Segment Note tab and places your cursor in the notes field, ready to type. Perfect for quick proofreading notes, context reminders, or translation decisions.
103
+
104
+ ### FIXED in v1.9.121 - 🐛 Find & Replace Performance
105
+
106
+ **Critical Fix:** v1.9.120 accidentally made Find & Replace slower (37+ seconds). v1.9.121 actually fixes it by updating cells in-place instead of recreating all widgets. Now near-instant!
107
+
108
+ ### OPTIMIZED in v1.9.120 - ⚡ Find & Replace Speed (BUGGY - USE v1.9.121)
109
+
110
+ ### ADDED in v1.9.119 - ⌨️ Alt+D Dictionary Shortcut
111
+
112
+ **Quick Dictionary Addition:** Press Alt+D to instantly add misspelled words to your custom dictionary without using the right-click menu.
113
+
114
+ ### FIXED in v1.9.117 - 🐛 Glossary Punctuation Matching
115
+
116
+ **Critical Fix:** Glossary entries with trailing punctuation (periods, quotes, etc.) now match correctly! Previously, "sentence." in glossary wouldn't match "sentence." in source text due to tokenization stripping punctuation from source but not from glossary entries.
83
117
 
84
118
  ### FIXED in v1.9.116 - 🐛 Tab Navigation & Startup
85
119