supervertaler 1.9.174__py3-none-any.whl → 1.9.176b0__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.
Supervertaler.py CHANGED
@@ -34,7 +34,7 @@ License: MIT
34
34
  """
35
35
 
36
36
  # Version Information.
37
- __version__ = "1.9.172"
37
+ __version__ = "1.9.176-beta"
38
38
  __phase__ = "0.9"
39
39
  __release_date__ = "2026-01-28"
40
40
  __edition__ = "Qt"
@@ -5794,11 +5794,14 @@ class PreTranslationWorker(QThread):
5794
5794
  custom_prompt = None
5795
5795
  if self.prompt_manager:
5796
5796
  try:
5797
+ # Get glossary terms for AI injection
5798
+ glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
5797
5799
  full_prompt = self.prompt_manager.build_final_prompt(
5798
5800
  source_text=segment.source,
5799
5801
  source_lang=source_lang,
5800
5802
  target_lang=target_lang,
5801
- mode="single"
5803
+ mode="single",
5804
+ glossary_terms=glossary_terms
5802
5805
  )
5803
5806
  # Extract just the instruction part (without the source text section)
5804
5807
  if "**SOURCE TEXT:**" in full_prompt:
@@ -5857,11 +5860,14 @@ class PreTranslationWorker(QThread):
5857
5860
  if self.prompt_manager and batch_segments:
5858
5861
  try:
5859
5862
  first_segment = batch_segments[0][1]
5863
+ # Get glossary terms for AI injection
5864
+ glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
5860
5865
  full_prompt = self.prompt_manager.build_final_prompt(
5861
5866
  source_text=first_segment.source,
5862
5867
  source_lang=source_lang,
5863
5868
  target_lang=target_lang,
5864
- mode="single"
5869
+ mode="single",
5870
+ glossary_terms=glossary_terms
5865
5871
  )
5866
5872
  # Extract just the instruction part
5867
5873
  if "**SOURCE TEXT:**" in full_prompt:
@@ -6308,6 +6314,10 @@ class SupervertalerQt(QMainWindow):
6308
6314
  # TM Metadata Manager - needed for TM list in Superlookup
6309
6315
  from modules.tm_metadata_manager import TMMetadataManager
6310
6316
  self.tm_metadata_mgr = TMMetadataManager(self.db_manager, self.log)
6317
+
6318
+ # Termbase Manager - needed for glossary AI injection
6319
+ from modules.termbase_manager import TermbaseManager
6320
+ self.termbase_mgr = TermbaseManager(self.db_manager, self.log)
6311
6321
 
6312
6322
  # Spellcheck Manager for target language spell checking
6313
6323
  self.spellcheck_manager = get_spellcheck_manager(str(self.user_data_path))
@@ -6425,13 +6435,10 @@ class SupervertalerQt(QMainWindow):
6425
6435
  from PyQt6.QtCore import QTimer
6426
6436
  QTimer.singleShot(2000, lambda: self._check_for_new_models(force=False)) # 2 second delay
6427
6437
 
6428
- # First-run check - show data location dialog, then Features tab
6429
- if self._needs_data_location_dialog:
6438
+ # First-run check - show unified setup wizard
6439
+ if self._needs_data_location_dialog or not general_settings.get('first_run_completed', False):
6430
6440
  from PyQt6.QtCore import QTimer
6431
- QTimer.singleShot(300, self._show_data_location_dialog)
6432
- elif not general_settings.get('first_run_completed', False):
6433
- from PyQt6.QtCore import QTimer
6434
- QTimer.singleShot(500, self._show_first_run_welcome)
6441
+ QTimer.singleShot(300, lambda: self._show_setup_wizard(is_first_run=True))
6435
6442
 
6436
6443
  def _show_data_location_dialog(self):
6437
6444
  """Show dialog to let user choose their data folder location on first run."""
@@ -6662,7 +6669,273 @@ class SupervertalerQt(QMainWindow):
6662
6669
  self.log("✅ First-run welcome shown (will show again next time)")
6663
6670
  except Exception as e:
6664
6671
  self.log(f"⚠️ First-run welcome error: {e}")
6665
-
6672
+
6673
+ def _show_setup_wizard(self, is_first_run: bool = False):
6674
+ """
6675
+ Show unified setup wizard that combines data folder selection and features intro.
6676
+
6677
+ Args:
6678
+ is_first_run: If True, this is an automatic first-run trigger. If False, user
6679
+ manually invoked from menu (skip data folder if already configured).
6680
+ """
6681
+ try:
6682
+ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
6683
+ QPushButton, QLineEdit, QFileDialog, QStackedWidget,
6684
+ QWidget, QFrame, QCheckBox)
6685
+ from PyQt6.QtCore import Qt
6686
+
6687
+ dialog = QDialog(self)
6688
+ dialog.setWindowTitle("Supervertaler Setup Wizard")
6689
+ dialog.setMinimumWidth(600)
6690
+ dialog.setMinimumHeight(450)
6691
+ dialog.setModal(True)
6692
+
6693
+ main_layout = QVBoxLayout(dialog)
6694
+ main_layout.setSpacing(15)
6695
+ main_layout.setContentsMargins(20, 20, 20, 20)
6696
+
6697
+ # Stacked widget for wizard pages
6698
+ stacked = QStackedWidget()
6699
+
6700
+ # Determine if we need to show data folder page
6701
+ show_data_folder_page = is_first_run and self._needs_data_location_dialog
6702
+
6703
+ # ==================== PAGE 1: Data Folder Selection ====================
6704
+ page1 = QWidget()
6705
+ page1_layout = QVBoxLayout(page1)
6706
+ page1_layout.setSpacing(15)
6707
+
6708
+ # Step indicator
6709
+ step1_indicator = QLabel("<span style='color: #888;'>Step 1 of 2</span>")
6710
+ page1_layout.addWidget(step1_indicator)
6711
+
6712
+ # Title
6713
+ page1_title = QLabel("<h2>📁 Choose Your Data Folder</h2>")
6714
+ page1_layout.addWidget(page1_title)
6715
+
6716
+ # Explanation
6717
+ page1_msg = QLabel(
6718
+ "Supervertaler stores your data in a folder of your choice:<br><br>"
6719
+ "• <b>API keys</b> – Your LLM provider credentials<br>"
6720
+ "• <b>Translation memories</b> – Reusable translation pairs<br>"
6721
+ "• <b>Glossaries</b> – Terminology databases<br>"
6722
+ "• <b>Prompts</b> – Custom AI prompts<br>"
6723
+ "• <b>Settings</b> – Application configuration<br><br>"
6724
+ "Choose a location that's easy to find and backup."
6725
+ )
6726
+ page1_msg.setWordWrap(True)
6727
+ page1_layout.addWidget(page1_msg)
6728
+
6729
+ # Path input with browse button
6730
+ path_layout = QHBoxLayout()
6731
+ path_edit = QLineEdit()
6732
+ default_path = get_default_user_data_path()
6733
+ path_edit.setText(str(default_path))
6734
+ path_edit.setMinimumWidth(350)
6735
+ path_layout.addWidget(path_edit)
6736
+
6737
+ browse_btn = QPushButton("Browse...")
6738
+ def browse_folder():
6739
+ folder = QFileDialog.getExistingDirectory(
6740
+ dialog,
6741
+ "Choose Data Folder",
6742
+ str(Path.home())
6743
+ )
6744
+ if folder:
6745
+ folder_path = Path(folder)
6746
+ if folder_path.name != "Supervertaler":
6747
+ folder_path = folder_path / "Supervertaler"
6748
+ path_edit.setText(str(folder_path))
6749
+
6750
+ browse_btn.clicked.connect(browse_folder)
6751
+ path_layout.addWidget(browse_btn)
6752
+ page1_layout.addLayout(path_layout)
6753
+
6754
+ # Tip
6755
+ page1_tip = QLabel(
6756
+ "💡 <b>Tip:</b> The default location is in your home folder, "
6757
+ "making it easy to find and backup."
6758
+ )
6759
+ page1_tip.setWordWrap(True)
6760
+ page1_tip.setStyleSheet("color: #666;")
6761
+ page1_layout.addWidget(page1_tip)
6762
+
6763
+ page1_layout.addStretch()
6764
+ stacked.addWidget(page1)
6765
+
6766
+ # ==================== PAGE 2: Features Introduction ====================
6767
+ page2 = QWidget()
6768
+ page2_layout = QVBoxLayout(page2)
6769
+ page2_layout.setSpacing(15)
6770
+
6771
+ # Step indicator
6772
+ step2_label = "Step 2 of 2" if show_data_folder_page else "Setup"
6773
+ step2_indicator = QLabel(f"<span style='color: #888;'>{step2_label}</span>")
6774
+ page2_layout.addWidget(step2_indicator)
6775
+
6776
+ # Data folder info (shown when skipping page 1)
6777
+ if not show_data_folder_page:
6778
+ from PyQt6.QtGui import QDesktopServices
6779
+ from PyQt6.QtCore import QUrl
6780
+
6781
+ data_folder_path = str(self.user_data_path)
6782
+ data_folder_info = QLabel(
6783
+ f"<b>📁 Data Folder:</b> <a href='file:///{data_folder_path}' "
6784
+ f"style='color: #3b82f6;'>{data_folder_path}</a><br>"
6785
+ "<span style='color: #666; font-size: 0.9em;'>"
6786
+ "Your settings, TMs, glossaries and prompts are stored here. "
6787
+ "Change in Settings → General.</span>"
6788
+ )
6789
+ data_folder_info.setWordWrap(True)
6790
+ data_folder_info.setTextFormat(Qt.TextFormat.RichText)
6791
+ data_folder_info.setOpenExternalLinks(False) # Handle clicks ourselves
6792
+ data_folder_info.linkActivated.connect(
6793
+ lambda url: QDesktopServices.openUrl(QUrl.fromLocalFile(data_folder_path))
6794
+ )
6795
+ data_folder_info.setStyleSheet(
6796
+ "background: #f0f4ff; padding: 12px; border-radius: 6px; "
6797
+ "border-left: 4px solid #3b82f6; margin-bottom: 10px;"
6798
+ )
6799
+ page2_layout.addWidget(data_folder_info)
6800
+
6801
+ # Title
6802
+ page2_title = QLabel("<h2>✨ Modular Features</h2>")
6803
+ page2_layout.addWidget(page2_title)
6804
+
6805
+ # Message
6806
+ page2_msg = QLabel(
6807
+ "Supervertaler uses a <b>modular architecture</b> – you can install "
6808
+ "only the features you need.<br><br>"
6809
+ "<b>Core features</b> (always available):<br>"
6810
+ "• AI translation with OpenAI, Claude, Gemini, Ollama<br>"
6811
+ "• Translation Memory and Glossaries<br>"
6812
+ "• XLIFF, SDLXLIFF, memoQ support<br>"
6813
+ "• Basic spellchecking<br><br>"
6814
+ "<b>Optional features</b> (install via pip):<br>"
6815
+ "• <code>openai-whisper</code> – Local voice dictation (no API needed)<br><br>"
6816
+ "You can view and manage features in <b>Settings → Features</b>."
6817
+ )
6818
+ page2_msg.setWordWrap(True)
6819
+ page2_msg.setTextFormat(Qt.TextFormat.RichText)
6820
+ page2_layout.addWidget(page2_msg)
6821
+
6822
+ # Checkbox
6823
+ dont_show_checkbox = CheckmarkCheckBox("Don't show this wizard on startup")
6824
+ dont_show_checkbox.setChecked(True)
6825
+ page2_layout.addWidget(dont_show_checkbox)
6826
+
6827
+ # Open Features tab checkbox
6828
+ open_features_checkbox = CheckmarkCheckBox("Open Features tab after closing")
6829
+ open_features_checkbox.setChecked(True)
6830
+ page2_layout.addWidget(open_features_checkbox)
6831
+
6832
+ page2_layout.addStretch()
6833
+ stacked.addWidget(page2)
6834
+
6835
+ main_layout.addWidget(stacked)
6836
+
6837
+ # ==================== Navigation Buttons ====================
6838
+ nav_layout = QHBoxLayout()
6839
+
6840
+ back_btn = QPushButton("← Back")
6841
+ back_btn.setVisible(False) # Hidden on first page
6842
+
6843
+ next_btn = QPushButton("Next →")
6844
+ finish_btn = QPushButton("Finish")
6845
+ finish_btn.setVisible(False)
6846
+ finish_btn.setDefault(True)
6847
+
6848
+ # Use Default button (only on page 1)
6849
+ default_btn = QPushButton("Use Default")
6850
+ default_btn.clicked.connect(lambda: path_edit.setText(str(default_path)))
6851
+
6852
+ nav_layout.addWidget(default_btn)
6853
+ nav_layout.addStretch()
6854
+ nav_layout.addWidget(back_btn)
6855
+ nav_layout.addWidget(next_btn)
6856
+ nav_layout.addWidget(finish_btn)
6857
+
6858
+ main_layout.addLayout(nav_layout)
6859
+
6860
+ # Track chosen path for later
6861
+ chosen_path_holder = [None]
6862
+
6863
+ def go_to_page(page_index):
6864
+ stacked.setCurrentIndex(page_index)
6865
+ if page_index == 0:
6866
+ back_btn.setVisible(False)
6867
+ next_btn.setVisible(True)
6868
+ finish_btn.setVisible(False)
6869
+ default_btn.setVisible(True)
6870
+ else:
6871
+ back_btn.setVisible(show_data_folder_page)
6872
+ next_btn.setVisible(False)
6873
+ finish_btn.setVisible(True)
6874
+ default_btn.setVisible(False)
6875
+
6876
+ def on_next():
6877
+ # Save the data folder choice
6878
+ chosen_path = Path(path_edit.text())
6879
+ chosen_path_holder[0] = chosen_path
6880
+
6881
+ # Create the folder and save config
6882
+ chosen_path.mkdir(parents=True, exist_ok=True)
6883
+ save_user_data_path(chosen_path)
6884
+
6885
+ # Update our path if different
6886
+ if chosen_path != self.user_data_path:
6887
+ self.user_data_path = chosen_path
6888
+ self._reinitialize_with_new_data_path()
6889
+ else:
6890
+ if hasattr(self, 'db_manager') and self.db_manager and not self.db_manager.connection:
6891
+ self.db_manager.connect()
6892
+
6893
+ self.log(f"📁 Data folder set to: {chosen_path}")
6894
+ go_to_page(1)
6895
+
6896
+ def on_back():
6897
+ go_to_page(0)
6898
+
6899
+ def on_finish():
6900
+ # Save first_run preference
6901
+ if dont_show_checkbox.isChecked():
6902
+ settings = self.load_general_settings()
6903
+ settings['first_run_completed'] = True
6904
+ self.save_general_settings(settings)
6905
+ self.log("✅ Setup wizard completed (won't show again on startup)")
6906
+ else:
6907
+ self.log("✅ Setup wizard shown (will show again next time)")
6908
+
6909
+ dialog.accept()
6910
+
6911
+ # Navigate to Features tab if checkbox is checked
6912
+ if open_features_checkbox.isChecked():
6913
+ self.main_tabs.setCurrentIndex(4) # Settings tab
6914
+ if hasattr(self, 'settings_tabs'):
6915
+ for i in range(self.settings_tabs.count()):
6916
+ if "Features" in self.settings_tabs.tabText(i):
6917
+ self.settings_tabs.setCurrentIndex(i)
6918
+ break
6919
+
6920
+ back_btn.clicked.connect(on_back)
6921
+ next_btn.clicked.connect(on_next)
6922
+ finish_btn.clicked.connect(on_finish)
6923
+
6924
+ # Start on appropriate page
6925
+ if show_data_folder_page:
6926
+ go_to_page(0)
6927
+ else:
6928
+ # Skip to features page if data folder already configured
6929
+ go_to_page(1)
6930
+ step2_indicator.setText("<span style='color: #888;'>Supervertaler Setup</span>")
6931
+
6932
+ dialog.exec()
6933
+
6934
+ except Exception as e:
6935
+ self.log(f"⚠️ Setup wizard error: {e}")
6936
+ import traceback
6937
+ traceback.print_exc()
6938
+
6666
6939
  def _check_for_new_models(self, force: bool = False):
6667
6940
  """
6668
6941
  Check for new LLM models from providers
@@ -7926,6 +8199,11 @@ class SupervertalerQt(QMainWindow):
7926
8199
  superdocs_action.triggered.connect(lambda: self._open_url("https://supervertaler.gitbook.io/superdocs/"))
7927
8200
  help_menu.addAction(superdocs_action)
7928
8201
 
8202
+ setup_wizard_action = QAction("🚀 Setup Wizard...", self)
8203
+ setup_wizard_action.setToolTip("Run the initial setup wizard (data folder location, features overview)")
8204
+ setup_wizard_action.triggered.connect(lambda: self._show_setup_wizard(is_first_run=False))
8205
+ help_menu.addAction(setup_wizard_action)
8206
+
7929
8207
  help_menu.addSeparator()
7930
8208
 
7931
8209
  shortcuts_action = QAction("⌨️ Keyboard Shortcuts", self)
@@ -12750,7 +13028,8 @@ class SupervertalerQt(QMainWindow):
12750
13028
  "💡 <b>Glossaries</b><br>"
12751
13029
  "• <b>Read</b> (green ✓): Glossary is used for terminology matching<br>"
12752
13030
  "• <b>Write</b> (blue ✓): Glossary is updated with new terms<br>"
12753
- "• <b>Priority</b>: Manually set 1-N (lower = higher priority). Multiple glossaries can share same priority. Priority #1 = Project Glossary."
13031
+ "• <b>Priority</b>: Manually set 1-N (lower = higher priority). Priority #1 = Project Glossary.<br>"
13032
+ "• <b>AI</b> (orange ✓): Send glossary terms to LLM with every translation (increases prompt size)"
12754
13033
  )
12755
13034
  help_msg.setWordWrap(True)
12756
13035
  help_msg.setStyleSheet("background-color: #e3f2fd; padding: 8px; border-radius: 4px; color: #1976d2;")
@@ -12772,8 +13051,8 @@ class SupervertalerQt(QMainWindow):
12772
13051
  # Termbase list with table
12773
13052
  termbase_table = QTableWidget()
12774
13053
  self.termbase_table = termbase_table # Store for external access (Superlookup navigation)
12775
- termbase_table.setColumnCount(7)
12776
- termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority"])
13054
+ termbase_table.setColumnCount(8)
13055
+ termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority", "AI"])
12777
13056
  termbase_table.horizontalHeader().setStretchLastSection(False)
12778
13057
  termbase_table.setColumnWidth(0, 80) # Type (Project/Background)
12779
13058
  termbase_table.setColumnWidth(1, 180) # Name
@@ -12782,6 +13061,7 @@ class SupervertalerQt(QMainWindow):
12782
13061
  termbase_table.setColumnWidth(4, 50) # Read checkbox
12783
13062
  termbase_table.setColumnWidth(5, 50) # Write checkbox
12784
13063
  termbase_table.setColumnWidth(6, 60) # Priority
13064
+ termbase_table.setColumnWidth(7, 40) # AI checkbox
12785
13065
  termbase_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
12786
13066
  termbase_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
12787
13067
  termbase_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Disable inline editing
@@ -13475,7 +13755,43 @@ class SupervertalerQt(QMainWindow):
13475
13755
  priority_item.setToolTip("No priority - glossary not readable")
13476
13756
  priority_item.setFlags(priority_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
13477
13757
  termbase_table.setItem(row, 6, priority_item)
13478
-
13758
+
13759
+ # AI checkbox (purple/orange) - whether to inject terms into LLM prompts
13760
+ ai_enabled = termbase_mgr.get_termbase_ai_inject(tb['id'])
13761
+ ai_checkbox = OrangeCheckmarkCheckBox()
13762
+ ai_checkbox.setChecked(ai_enabled)
13763
+ ai_checkbox.setToolTip("AI: Send glossary terms to LLM with translation prompts")
13764
+
13765
+ def on_ai_toggle(checked, tb_id=tb['id'], tb_name=tb['name']):
13766
+ if checked:
13767
+ # Show warning when enabling
13768
+ from PyQt6.QtWidgets import QMessageBox
13769
+ msg = QMessageBox()
13770
+ msg.setWindowTitle("Enable AI Injection")
13771
+ msg.setText(f"Enable AI injection for '{tb_name}'?")
13772
+ msg.setInformativeText(
13773
+ "When enabled, ALL terms from this glossary will be sent to the LLM "
13774
+ "with every translation request.\n\n"
13775
+ "This helps the AI consistently use your preferred terminology "
13776
+ "throughout the translation.\n\n"
13777
+ "Recommended for small, curated glossaries (< 500 terms)."
13778
+ )
13779
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
13780
+ msg.setDefaultButton(QMessageBox.StandardButton.Yes)
13781
+ if msg.exec() != QMessageBox.StandardButton.Yes:
13782
+ # User cancelled - revert checkbox
13783
+ sender = termbase_table.cellWidget(termbase_table.currentRow(), 7)
13784
+ if sender:
13785
+ sender.blockSignals(True)
13786
+ sender.setChecked(False)
13787
+ sender.blockSignals(False)
13788
+ return
13789
+ termbase_mgr.set_termbase_ai_inject(tb_id, checked)
13790
+ self.log(f"{'✅ Enabled' if checked else '❌ Disabled'} AI injection for glossary: {tb_name}")
13791
+
13792
+ ai_checkbox.toggled.connect(on_ai_toggle)
13793
+ termbase_table.setCellWidget(row, 7, ai_checkbox)
13794
+
13479
13795
  # Update header checkbox states based on current selection
13480
13796
  tb_read_header_checkbox.blockSignals(True)
13481
13797
  tb_write_header_checkbox.blockSignals(True)
@@ -31565,9 +31881,14 @@ class SupervertalerQt(QMainWindow):
31565
31881
 
31566
31882
  # Get source text
31567
31883
  source_text = current_segment.source
31568
-
31884
+
31885
+ # Get glossary terms for AI injection
31886
+ glossary_terms = self.get_ai_inject_glossary_terms()
31887
+
31569
31888
  # Build combined prompt
31570
- combined = self.prompt_manager_qt.build_final_prompt(source_text, source_lang, target_lang)
31889
+ combined = self.prompt_manager_qt.build_final_prompt(
31890
+ source_text, source_lang, target_lang, glossary_terms=glossary_terms
31891
+ )
31571
31892
 
31572
31893
  # Check for figure/image context
31573
31894
  figure_info = ""
@@ -31594,11 +31915,14 @@ class SupervertalerQt(QMainWindow):
31594
31915
  composition_parts.append(f"📏 Total prompt: {len(combined):,} characters")
31595
31916
 
31596
31917
  if self.prompt_manager_qt.library.active_primary_prompt:
31597
- composition_parts.append(f"✓ Primary prompt attached")
31918
+ composition_parts.append(f"✓ Custom prompt attached")
31598
31919
 
31599
31920
  if self.prompt_manager_qt.library.attached_prompts:
31600
31921
  composition_parts.append(f"✓ {len(self.prompt_manager_qt.library.attached_prompts)} additional prompt(s) attached")
31601
-
31922
+
31923
+ if glossary_terms:
31924
+ composition_parts.append(f"📚 {len(glossary_terms)} glossary term(s) injected")
31925
+
31602
31926
  if figure_info:
31603
31927
  composition_parts.append(figure_info)
31604
31928
 
@@ -31633,12 +31957,59 @@ class SupervertalerQt(QMainWindow):
31633
31957
  image_notice.setStyleSheet("padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 4px solid #ff9800;")
31634
31958
  layout.addWidget(image_notice)
31635
31959
 
31636
- # Text editor for preview
31960
+ # Text editor for preview with syntax highlighting
31637
31961
  text_edit = QTextEdit()
31638
- text_edit.setPlainText(combined)
31639
31962
  text_edit.setReadOnly(True)
31640
31963
  text_edit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
31641
31964
  text_edit.setStyleSheet("font-family: 'Consolas', 'Courier New', monospace; font-size: 9pt;")
31965
+
31966
+ # Format the prompt with color highlighting
31967
+ import html
31968
+ import re
31969
+
31970
+ # Escape HTML entities first
31971
+ formatted_html = html.escape(combined)
31972
+
31973
+ # Replace newlines with <br> for HTML
31974
+ formatted_html = formatted_html.replace('\n', '<br>')
31975
+
31976
+ # Make "# SYSTEM PROMPT" bold and red
31977
+ formatted_html = re.sub(
31978
+ r'(#\s*SYSTEM\s*PROMPT)',
31979
+ r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
31980
+ formatted_html,
31981
+ flags=re.IGNORECASE
31982
+ )
31983
+
31984
+ # Make "# CUSTOM PROMPT" bold and red
31985
+ formatted_html = re.sub(
31986
+ r'(#\s*CUSTOM\s*PROMPT)',
31987
+ r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
31988
+ formatted_html,
31989
+ flags=re.IGNORECASE
31990
+ )
31991
+
31992
+ # Make "# GLOSSARY" bold and orange
31993
+ formatted_html = re.sub(
31994
+ r'(#\s*GLOSSARY)',
31995
+ r'<span style="color: #FF9800; font-weight: bold; font-size: 11pt;">\1</span>',
31996
+ formatted_html,
31997
+ flags=re.IGNORECASE
31998
+ )
31999
+
32000
+ # Make source text section blue (pattern: "XX text:<br>..." until double line break or # header)
32001
+ # Match language code + " text:" followed by content until "# " or end
32002
+ formatted_html = re.sub(
32003
+ r'(\w{2,5}\s+text:)(<br>)(.*?)(<br><br>(?:#|\*\*YOUR TRANSLATION)|$)',
32004
+ r'<span style="color: #1565c0; font-weight: bold;">\1</span>\2<span style="color: #1565c0;">\3</span>\4',
32005
+ formatted_html,
32006
+ flags=re.DOTALL
32007
+ )
32008
+
32009
+ # Wrap in pre-like styling div
32010
+ formatted_html = f'<div style="font-family: Consolas, Courier New, monospace; white-space: pre-wrap;">{formatted_html}</div>'
32011
+
32012
+ text_edit.setHtml(formatted_html)
31642
32013
  layout.addWidget(text_edit, 1)
31643
32014
 
31644
32015
  # Close button
@@ -35380,7 +35751,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35380
35751
  primary_prompt_text = f"✅ {prompt_path}"
35381
35752
  attached_count = len(library.attached_prompt_paths) if library.attached_prompt_paths else 0
35382
35753
 
35383
- ai_layout.addWidget(QLabel(f"<b>Primary Prompt:</b> {primary_prompt_text}"))
35754
+ ai_layout.addWidget(QLabel(f"<b>Custom Prompt:</b> {primary_prompt_text}"))
35384
35755
  if attached_count > 0:
35385
35756
  ai_layout.addWidget(QLabel(f"<b>Attached Prompts:</b> {attached_count}"))
35386
35757
 
@@ -40466,13 +40837,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
40466
40837
  except Exception as e:
40467
40838
  self.log(f"⚠ Could not add surrounding segments: {e}")
40468
40839
 
40840
+ # Get glossary terms for AI injection
40841
+ glossary_terms = self.get_ai_inject_glossary_terms()
40842
+
40469
40843
  custom_prompt = self.prompt_manager_qt.build_final_prompt(
40470
40844
  source_text=segment.source,
40471
40845
  source_lang=self.current_project.source_lang,
40472
40846
  target_lang=self.current_project.target_lang,
40473
- mode="single"
40847
+ mode="single",
40848
+ glossary_terms=glossary_terms
40474
40849
  )
40475
-
40850
+
40476
40851
  # Add surrounding context before the translation delimiter
40477
40852
  if surrounding_context:
40478
40853
  # Insert before the "YOUR TRANSLATION" delimiter
@@ -42023,11 +42398,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42023
42398
  if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt and batch_segments:
42024
42399
  try:
42025
42400
  first_segment = batch_segments[0][1]
42401
+ # Get glossary terms for AI injection
42402
+ glossary_terms = self.get_ai_inject_glossary_terms()
42026
42403
  base_prompt = self.prompt_manager_qt.build_final_prompt(
42027
42404
  source_text=first_segment.source,
42028
42405
  source_lang=source_lang,
42029
42406
  target_lang=target_lang,
42030
- mode="single"
42407
+ mode="single",
42408
+ glossary_terms=glossary_terms
42031
42409
  )
42032
42410
  if "**SOURCE TEXT:**" in base_prompt:
42033
42411
  base_prompt = base_prompt.split("**SOURCE TEXT:**")[0].strip()
@@ -42455,11 +42833,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42455
42833
  # Access parent through closure
42456
42834
  parent = self
42457
42835
  if hasattr(parent, 'prompt_manager_qt') and parent.prompt_manager_qt:
42836
+ # Get glossary terms for AI injection
42837
+ glossary_terms = parent.get_ai_inject_glossary_terms() if hasattr(parent, 'get_ai_inject_glossary_terms') else []
42458
42838
  custom_prompt = parent.prompt_manager_qt.build_final_prompt(
42459
42839
  source_text=source_text,
42460
42840
  source_lang=source_lang,
42461
42841
  target_lang=target_lang,
42462
- mode="single"
42842
+ mode="single",
42843
+ glossary_terms=glossary_terms
42463
42844
  )
42464
42845
  except Exception as e:
42465
42846
  self.log(f"⚠ Could not build LLM prompt from manager: {e}")
@@ -42942,7 +43323,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42942
43323
  api_keys['google'] = api_keys['gemini']
42943
43324
 
42944
43325
  return api_keys
42945
-
43326
+
43327
+ def get_ai_inject_glossary_terms(self) -> list:
43328
+ """Get glossary terms from AI-inject-enabled termbases for the current project.
43329
+
43330
+ Returns:
43331
+ List of term dictionaries with source_term, target_term, forbidden keys
43332
+ """
43333
+ if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
43334
+ return []
43335
+
43336
+ project_id = None
43337
+ if hasattr(self, 'current_project') and self.current_project:
43338
+ project_id = getattr(self.current_project, 'id', None)
43339
+
43340
+ return self.termbase_mgr.get_ai_inject_terms(project_id)
43341
+
42946
43342
  def ensure_example_api_keys(self):
42947
43343
  """Create example API keys file on first launch for new users"""
42948
43344
  example_file = self.user_data_path / "api_keys.example.txt"
@@ -48257,7 +48653,87 @@ class BlueCheckmarkCheckBox(QCheckBox):
48257
48653
 
48258
48654
  painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
48259
48655
  painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
48260
-
48656
+
48657
+ painter.end()
48658
+
48659
+
48660
+ class OrangeCheckmarkCheckBox(QCheckBox):
48661
+ """Custom checkbox with orange background and white checkmark when checked (for AI injection)"""
48662
+
48663
+ def __init__(self, text="", parent=None):
48664
+ super().__init__(text, parent)
48665
+ self.setCheckable(True)
48666
+ self.setEnabled(True)
48667
+ self.setStyleSheet("""
48668
+ QCheckBox {
48669
+ font-size: 9pt;
48670
+ spacing: 6px;
48671
+ }
48672
+ QCheckBox::indicator {
48673
+ width: 16px;
48674
+ height: 16px;
48675
+ border: 2px solid #999;
48676
+ border-radius: 3px;
48677
+ background-color: white;
48678
+ }
48679
+ QCheckBox::indicator:checked {
48680
+ background-color: #FF9800;
48681
+ border-color: #FF9800;
48682
+ }
48683
+ QCheckBox::indicator:hover {
48684
+ border-color: #666;
48685
+ }
48686
+ QCheckBox::indicator:checked:hover {
48687
+ background-color: #F57C00;
48688
+ border-color: #F57C00;
48689
+ }
48690
+ """)
48691
+
48692
+ def paintEvent(self, event):
48693
+ """Override paint event to draw white checkmark when checked"""
48694
+ super().paintEvent(event)
48695
+
48696
+ if self.isChecked():
48697
+ from PyQt6.QtWidgets import QStyleOptionButton
48698
+ from PyQt6.QtGui import QPainter, QPen, QColor
48699
+ from PyQt6.QtCore import QPointF
48700
+
48701
+ opt = QStyleOptionButton()
48702
+ self.initStyleOption(opt)
48703
+ indicator_rect = self.style().subElementRect(
48704
+ self.style().SubElement.SE_CheckBoxIndicator,
48705
+ opt,
48706
+ self
48707
+ )
48708
+
48709
+ if indicator_rect.isValid():
48710
+ painter = QPainter(self)
48711
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
48712
+ pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
48713
+ painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
48714
+ painter.setBrush(QColor(255, 255, 255))
48715
+
48716
+ x = indicator_rect.x()
48717
+ y = indicator_rect.y()
48718
+ w = indicator_rect.width()
48719
+ h = indicator_rect.height()
48720
+
48721
+ padding = min(w, h) * 0.15
48722
+ x += padding
48723
+ y += padding
48724
+ w -= padding * 2
48725
+ h -= padding * 2
48726
+
48727
+ check_x1 = x + w * 0.10
48728
+ check_y1 = y + h * 0.50
48729
+ check_x2 = x + w * 0.35
48730
+ check_y2 = y + h * 0.70
48731
+ check_x3 = x + w * 0.90
48732
+ check_y3 = y + h * 0.25
48733
+
48734
+ painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
48735
+ painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
48736
+
48261
48737
  painter.end()
48262
48738
 
48263
48739