supervertaler 1.9.114__py3-none-any.whl → 1.9.128__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,9 +18,15 @@ class ShortcutManager:
18
18
  "file_new": {
19
19
  "category": "File",
20
20
  "description": "New Project",
21
- "default": "Ctrl+N",
21
+ "default": "",
22
22
  "action": "new_project"
23
23
  },
24
+ "editor_focus_notes": {
25
+ "category": "Edit",
26
+ "description": "Focus Segment Note Tab (Ctrl+N)",
27
+ "default": "Ctrl+N",
28
+ "action": "focus_segment_notes"
29
+ },
24
30
  "file_open": {
25
31
  "category": "File",
26
32
  "description": "Open Project",
@@ -542,6 +548,13 @@ class ShortcutManager:
542
548
  "action": "copy_source_to_target",
543
549
  "context": "grid_editor"
544
550
  },
551
+ "editor_add_to_dictionary": {
552
+ "category": "Editor",
553
+ "description": "Add word at cursor to custom dictionary",
554
+ "default": "Alt+D",
555
+ "action": "add_word_to_dictionary",
556
+ "context": "grid_editor"
557
+ },
545
558
 
546
559
  # Filter Operations
547
560
  "filter_selected_text": {
@@ -941,13 +941,20 @@ class TermviewWidget(QWidget):
941
941
  for quote_char in '\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A':
942
942
  normalized_text = normalized_text.replace(quote_char, ' ')
943
943
 
944
+ # CRITICAL FIX v1.9.118: Strip punctuation from glossary term before matching
945
+ # This allows entries like "...problemen." (with period) to match source text
946
+ # where tokenization strips the period during word splitting
947
+ # Comprehensive set of quote and punctuation characters to strip
948
+ PUNCT_CHARS = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A'
949
+ normalized_term = source_lower.rstrip(PUNCT_CHARS).lstrip(PUNCT_CHARS)
950
+
944
951
  # Use word boundaries to match complete words/phrases only
945
952
  if ' ' in source_term:
946
953
  # Multi-word term - must exist as exact phrase
947
- pattern = r'\b' + re.escape(source_lower) + r'\b'
954
+ pattern = r'\b' + re.escape(normalized_term) + r'\b'
948
955
  else:
949
956
  # Single word
950
- pattern = r'\b' + re.escape(source_lower) + r'\b'
957
+ pattern = r'\b' + re.escape(normalized_term) + r'\b'
951
958
 
952
959
  # Try matching on normalized text first, then original
953
960
  if not re.search(pattern, normalized_text) and not re.search(pattern, text_lower):
@@ -171,11 +171,14 @@ class UnifiedPromptLibrary:
171
171
  # Backward compatibility: quick_run is the legacy field; internally we
172
172
  # treat it as the "QuickMenu (future app menu)" flag.
173
173
  prompt_data.setdefault('quick_run', False)
174
- prompt_data['quickmenu_quickmenu'] = bool(
175
- prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False))
174
+ # Support legacy quickmenu_quickmenu field (rename to sv_quickmenu)
175
+ if 'quickmenu_quickmenu' in prompt_data:
176
+ prompt_data['sv_quickmenu'] = prompt_data['quickmenu_quickmenu']
177
+ prompt_data['sv_quickmenu'] = bool(
178
+ prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False))
176
179
  )
177
180
  # Keep legacy field in sync so older code/versions still behave.
178
- prompt_data['quick_run'] = bool(prompt_data['quickmenu_quickmenu'])
181
+ prompt_data['quick_run'] = bool(prompt_data['sv_quickmenu'])
179
182
 
180
183
  # New QuickMenu fields
181
184
  prompt_data.setdefault('quickmenu_grid', False)
@@ -270,7 +273,7 @@ class UnifiedPromptLibrary:
270
273
  'name', 'description', 'domain', 'version', 'task_type',
271
274
  'favorite',
272
275
  # QuickMenu
273
- 'quickmenu_label', 'quickmenu_grid', 'quickmenu_quickmenu',
276
+ 'quickmenu_label', 'quickmenu_grid', 'sv_quickmenu',
274
277
  # Legacy (kept for backward compatibility)
275
278
  'quick_run',
276
279
  'folder', 'tags',
@@ -309,8 +312,8 @@ class UnifiedPromptLibrary:
309
312
  prompt_data['_relative_path'] = relative_path
310
313
 
311
314
  # Keep legacy field in sync
312
- if 'quickmenu_quickmenu' in prompt_data:
313
- prompt_data['quick_run'] = bool(prompt_data.get('quickmenu_quickmenu', False))
315
+ if 'sv_quickmenu' in prompt_data:
316
+ prompt_data['quick_run'] = bool(prompt_data.get('sv_quickmenu', False))
314
317
  self.prompts[relative_path] = prompt_data
315
318
 
316
319
  self.log(f"✓ Saved prompt: {prompt_data.get('name', relative_path)}")
@@ -456,8 +459,8 @@ class UnifiedPromptLibrary:
456
459
  return False
457
460
 
458
461
  prompt_data = self.prompts[relative_path]
459
- new_value = not bool(prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)))
460
- prompt_data['quickmenu_quickmenu'] = new_value
462
+ new_value = not bool(prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)))
463
+ prompt_data['sv_quickmenu'] = new_value
461
464
  prompt_data['quick_run'] = new_value # keep legacy in sync
462
465
  prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
463
466
 
@@ -493,7 +496,7 @@ class UnifiedPromptLibrary:
493
496
  """Update cached QuickMenu (future app menu) list (legacy name: quick_run)."""
494
497
  self._quick_run = []
495
498
  for path, data in self.prompts.items():
496
- is_enabled = bool(data.get('quickmenu_quickmenu', data.get('quick_run', False)))
499
+ is_enabled = bool(data.get('sv_quickmenu', data.get('quick_run', False)))
497
500
  if not is_enabled:
498
501
  continue
499
502
  label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
@@ -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,143 @@ 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
+ "{{DOCUMENT_CONTEXT}}",
912
+ "Formatted list of project segments (configurable % in Settings → AI Settings)",
913
+ "[1] Source text\\n → Target text\\n\\n[2] Source text\\n → Target text\\n\\n..."
914
+ )
915
+ ]
916
+
917
+ table.setRowCount(len(placeholders))
918
+ for row, (placeholder, description, example) in enumerate(placeholders):
919
+ # Placeholder column (monospace, bold)
920
+ item_placeholder = QTableWidgetItem(placeholder)
921
+ item_placeholder.setFont(QFont("Courier New", 10, QFont.Weight.Bold))
922
+ table.setItem(row, 0, item_placeholder)
923
+
924
+ # Description column
925
+ item_desc = QTableWidgetItem(description)
926
+ item_desc.setToolTip(description)
927
+ table.setItem(row, 1, item_desc)
928
+
929
+ # Example column (monospace, italic)
930
+ item_example = QTableWidgetItem(example)
931
+ item_example.setFont(QFont("Courier New", 9))
932
+ item_example.setToolTip(example)
933
+ table.setItem(row, 2, item_example)
934
+
935
+ # Set column widths
936
+ table.setColumnWidth(0, 200)
937
+ table.setColumnWidth(1, 300)
938
+ header = table.horizontalHeader()
939
+ header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
940
+ header.setSectionResizeMode(1, QHeaderView.ResizeMode.Interactive)
941
+ header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
942
+
943
+ # Adjust row heights for readability
944
+ for row in range(table.rowCount()):
945
+ table.setRowHeight(row, 60)
946
+
947
+ splitter.addWidget(table)
948
+
949
+ # Right: Usage tips panel
950
+ tips_panel = QWidget()
951
+ tips_layout = QVBoxLayout(tips_panel)
952
+ tips_layout.setContentsMargins(10, 0, 0, 0)
953
+
954
+ tips_header = QLabel("💡 Usage Tips")
955
+ tips_header.setStyleSheet("font-weight: bold; font-size: 11pt; color: #2196F3; margin-bottom: 8px;")
956
+ tips_layout.addWidget(tips_header)
957
+
958
+ tips_intro = QLabel(
959
+ "Use these placeholders in your prompts. They will be replaced with actual values when the prompt runs."
960
+ )
961
+ tips_intro.setWordWrap(True)
962
+ tips_intro.setStyleSheet("color: #666; margin-bottom: 15px; font-style: italic;")
963
+ tips_layout.addWidget(tips_intro)
964
+
965
+ tips_text = QLabel(
966
+ "• Placeholders are case-sensitive (use UPPERCASE)\n\n"
967
+ "• Surround placeholders with double curly braces: {{ }}\n\n"
968
+ "• You can combine multiple placeholders in one prompt\n\n"
969
+ "• Use {{DOCUMENT_CONTEXT}} for context-aware translations\n\n"
970
+ "• Configure {{DOCUMENT_CONTEXT}} percentage in Settings → AI Settings"
971
+ )
972
+ tips_text.setWordWrap(True)
973
+ tips_text.setStyleSheet("color: #666; line-height: 1.6;")
974
+ tips_layout.addWidget(tips_text)
975
+
976
+ tips_layout.addStretch()
977
+
978
+ tips_panel.setMinimumWidth(280)
979
+ tips_panel.setMaximumWidth(400)
980
+ splitter.addWidget(tips_panel)
981
+
982
+ # Set splitter proportions (75% table, 25% tips)
983
+ splitter.setSizes([750, 250])
984
+ splitter.setStretchFactor(0, 1) # Table expands
985
+ splitter.setStretchFactor(1, 0) # Tips panel fixed-ish
986
+
987
+ layout.addWidget(splitter, 1) # 1 = stretch to fill all available space
988
+
989
+ return tab
990
+
767
991
  def _create_context_sidebar(self) -> QWidget:
768
992
  """Create context sidebar showing available resources"""
769
993
  panel = QWidget()
@@ -1470,10 +1694,10 @@ class UnifiedPromptManagerQt:
1470
1694
  self.editor_quickmenu_label_input.setPlaceholderText("Label shown in QuickMenu")
1471
1695
  quickmenu_layout.addWidget(self.editor_quickmenu_label_input, 2)
1472
1696
 
1473
- self.editor_quickmenu_in_grid_cb = QCheckBox("Show in Grid right-click QuickMenu")
1697
+ self.editor_quickmenu_in_grid_cb = CheckmarkCheckBox("Show in Grid right-click QuickMenu")
1474
1698
  quickmenu_layout.addWidget(self.editor_quickmenu_in_grid_cb, 2)
1475
1699
 
1476
- self.editor_quickmenu_in_quickmenu_cb = QCheckBox("Show in QuickMenu")
1700
+ self.editor_quickmenu_in_quickmenu_cb = CheckmarkCheckBox("Show in Supervertaler QuickMenu")
1477
1701
  quickmenu_layout.addWidget(self.editor_quickmenu_in_quickmenu_cb, 1)
1478
1702
 
1479
1703
  layout.addLayout(quickmenu_layout)
@@ -1950,7 +2174,7 @@ class UnifiedPromptManagerQt:
1950
2174
  indicators = []
1951
2175
  if prompt_data.get('favorite'):
1952
2176
  indicators.append("⭐")
1953
- if prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)):
2177
+ if prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)):
1954
2178
  indicators.append("⚡")
1955
2179
  if prompt_data.get('quickmenu_grid', False):
1956
2180
  indicators.append("🖱️")
@@ -2018,7 +2242,7 @@ class UnifiedPromptManagerQt:
2018
2242
  action_fav.triggered.connect(lambda: self._toggle_favorite(path))
2019
2243
 
2020
2244
  # Toggle QuickMenu (legacy: quick_run)
2021
- if prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)):
2245
+ if prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)):
2022
2246
  action_qr = menu.addAction("⚡ Remove from QuickMenu")
2023
2247
  else:
2024
2248
  action_qr = menu.addAction("⚡ Add to QuickMenu")
@@ -2070,7 +2294,7 @@ class UnifiedPromptManagerQt:
2070
2294
  if hasattr(self, 'editor_quickmenu_in_grid_cb'):
2071
2295
  self.editor_quickmenu_in_grid_cb.setChecked(bool(prompt_data.get('quickmenu_grid', False)))
2072
2296
  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))))
2297
+ self.editor_quickmenu_in_quickmenu_cb.setChecked(bool(prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False))))
2074
2298
  self.editor_content.setPlainText(prompt_data.get('content', ''))
2075
2299
 
2076
2300
  # Store current path for saving
@@ -2083,119 +2307,127 @@ class UnifiedPromptManagerQt:
2083
2307
 
2084
2308
  def _save_current_prompt(self):
2085
2309
  """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
2310
+ try:
2311
+ name = self.editor_name_input.text().strip()
2312
+ description = self.editor_desc_input.text().strip()
2313
+ content = self.editor_content.toPlainText().strip()
2116
2314
 
2117
- # Editing existing library prompt
2118
- if path not in self.library.prompts:
2119
- QMessageBox.warning(self.main_widget, "Error", "Prompt no longer exists")
2315
+ # Name field now represents the complete filename with extension
2316
+ # No stripping needed - user sees and edits the full filename
2317
+
2318
+ quickmenu_label = ''
2319
+ quickmenu_grid = False
2320
+ sv_quickmenu = False
2321
+ if hasattr(self, 'editor_quickmenu_label_input'):
2322
+ quickmenu_label = self.editor_quickmenu_label_input.text().strip()
2323
+ if hasattr(self, 'editor_quickmenu_in_grid_cb'):
2324
+ quickmenu_grid = bool(self.editor_quickmenu_in_grid_cb.isChecked())
2325
+ if hasattr(self, 'editor_quickmenu_in_quickmenu_cb'):
2326
+ sv_quickmenu = bool(self.editor_quickmenu_in_quickmenu_cb.isChecked())
2327
+
2328
+ if not name or not content:
2329
+ QMessageBox.warning(self.main_widget, "Error", "Name and content are required")
2120
2330
  return
2121
2331
 
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
2332
+ # Check if this is a new prompt or editing existing
2333
+ if hasattr(self, 'editor_current_path') and self.editor_current_path:
2334
+ path = self.editor_current_path
2143
2335
 
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}")
2336
+ # Handle external prompts (save back to external file)
2337
+ if path.startswith("[EXTERNAL] "):
2338
+ external_file_path = path[11:] # Remove "[EXTERNAL] " prefix
2339
+ self._save_external_prompt(external_file_path, name, description, content)
2340
+ return
2341
+
2342
+ # Editing existing library prompt
2343
+ if path not in self.library.prompts:
2344
+ QMessageBox.warning(self.main_widget, "Error", "Prompt no longer exists")
2345
+ return
2346
+
2347
+ prompt_data = self.library.prompts[path].copy()
2348
+ old_filename = Path(path).name
2349
+
2350
+ # Extract name without extension for metadata
2351
+ name_without_ext = Path(name).stem
2352
+
2353
+ prompt_data['name'] = name_without_ext
2354
+ prompt_data['description'] = description
2355
+ prompt_data['content'] = content
2356
+ prompt_data['quickmenu_label'] = quickmenu_label or name_without_ext
2357
+ prompt_data['quickmenu_grid'] = quickmenu_grid
2358
+ prompt_data['sv_quickmenu'] = sv_quickmenu
2359
+ # Keep legacy field in sync
2360
+ prompt_data['quick_run'] = sv_quickmenu
2361
+
2362
+ # Check if filename changed - need to rename file
2363
+ if old_filename != name:
2364
+ old_path = Path(path)
2365
+ folder = str(old_path.parent) if old_path.parent != Path('.') else ''
2366
+ new_path = f"{folder}/{name}" if folder else name
2367
+
2368
+ # Delete old file and save to new location
2369
+ if self.library.delete_prompt(path):
2370
+ if self.library.save_prompt(new_path, prompt_data):
2371
+ self.library.load_all_prompts()
2372
+ self._refresh_tree()
2373
+ self._select_and_reveal_prompt(new_path)
2374
+ self.editor_current_path = new_path # Update to new path
2375
+ QMessageBox.information(self.main_widget, "Saved", f"Prompt renamed to '{name}' successfully!")
2376
+ self.log_message(f"✓ Renamed prompt: {old_filename} → {name}")
2377
+ else:
2378
+ QMessageBox.warning(self.main_widget, "Error", "Failed to rename prompt")
2153
2379
  else:
2154
- QMessageBox.warning(self.main_widget, "Error", "Failed to rename prompt")
2380
+ QMessageBox.warning(self.main_widget, "Error", "Failed to delete old prompt file")
2155
2381
  else:
2156
- QMessageBox.warning(self.main_widget, "Error", "Failed to delete old prompt file")
2382
+ # Name unchanged, just update in place
2383
+ if self.library.save_prompt(path, prompt_data):
2384
+ QMessageBox.information(self.main_widget, "Saved", "Prompt updated successfully!")
2385
+ self._refresh_tree()
2386
+ else:
2387
+ QMessageBox.warning(self.main_widget, "Error", "Failed to save prompt")
2157
2388
  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!")
2389
+ # Creating new prompt
2390
+ folder = getattr(self, 'editor_target_folder', 'Project Prompts')
2391
+
2392
+ # Create new prompt data
2393
+ from datetime import datetime
2394
+ prompt_data = {
2395
+ 'name': name,
2396
+ 'description': description,
2397
+ 'content': content,
2398
+ 'domain': '',
2399
+ 'version': '1.0',
2400
+ 'task_type': 'Translation',
2401
+ 'favorite': False,
2402
+ # QuickMenu
2403
+ 'quickmenu_label': quickmenu_label or name,
2404
+ 'quickmenu_grid': quickmenu_grid,
2405
+ 'sv_quickmenu': sv_quickmenu,
2406
+ # Legacy
2407
+ 'quick_run': sv_quickmenu,
2408
+ 'folder': folder,
2409
+ 'tags': [],
2410
+ 'created': datetime.now().strftime('%Y-%m-%d'),
2411
+ 'modified': datetime.now().strftime('%Y-%m-%d')
2412
+ }
2413
+
2414
+ # Create the prompt file (save_prompt creates new file if it doesn't exist)
2415
+ relative_path = f"{folder}/{name}.svprompt"
2416
+ if self.library.save_prompt(relative_path, prompt_data):
2417
+ QMessageBox.information(self.main_widget, "Created", f"Prompt '{name}' created successfully!")
2418
+ self.library.load_all_prompts() # Reload to get new prompt in memory
2161
2419
  self._refresh_tree()
2420
+ self.editor_current_path = relative_path # Now editing this prompt
2162
2421
  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")
2422
+ QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
2423
+
2424
+ except Exception as e:
2425
+ import traceback
2426
+ error_msg = f"Prompt save error: {str(e)}\n{traceback.format_exc()}"
2427
+ print(f"[ERROR] {error_msg}")
2428
+ self.log_message(f"❌ Prompt save error: {str(e)}")
2429
+ QMessageBox.critical(self.main_widget, "Save Error", f"Failed to save prompt:\n\n{str(e)}")
2430
+ return
2199
2431
 
2200
2432
  def _save_external_prompt(self, file_path: str, name: str, description: str, content: str):
2201
2433
  """Save changes to an external prompt file"""
@@ -2421,7 +2653,7 @@ class UnifiedPromptManagerQt:
2421
2653
  'favorite': False,
2422
2654
  'quickmenu_label': name_without_ext,
2423
2655
  'quickmenu_grid': False,
2424
- 'quickmenu_quickmenu': False,
2656
+ 'sv_quickmenu': False,
2425
2657
  'quick_run': False,
2426
2658
  'folder': folder_path,
2427
2659
  'tags': [],
@@ -2437,6 +2669,7 @@ class UnifiedPromptManagerQt:
2437
2669
  self._refresh_tree()
2438
2670
  self._select_and_reveal_prompt(relative_path)
2439
2671
  self._load_prompt_in_editor(relative_path)
2672
+ self.btn_save_prompt.setEnabled(True) # Ensure Save button is enabled for new prompt
2440
2673
  self.log_message(f"✓ Created new prompt '{name}' in folder: {folder_path}")
2441
2674
  else:
2442
2675
  QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
@@ -2469,7 +2702,7 @@ class UnifiedPromptManagerQt:
2469
2702
  src_data['favorite'] = False
2470
2703
  src_data['quick_run'] = False
2471
2704
  src_data['quickmenu_grid'] = False
2472
- src_data['quickmenu_quickmenu'] = False
2705
+ src_data['sv_quickmenu'] = False
2473
2706
  src_data['folder'] = folder
2474
2707
  src_data['created'] = datetime.now().strftime('%Y-%m-%d')
2475
2708
  src_data['modified'] = datetime.now().strftime('%Y-%m-%d')