supervertaler 1.9.174__py3-none-any.whl → 1.9.175__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.175"
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))
@@ -12750,7 +12760,8 @@ class SupervertalerQt(QMainWindow):
12750
12760
  "💡 <b>Glossaries</b><br>"
12751
12761
  "• <b>Read</b> (green ✓): Glossary is used for terminology matching<br>"
12752
12762
  "• <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."
12763
+ "• <b>Priority</b>: Manually set 1-N (lower = higher priority). Priority #1 = Project Glossary.<br>"
12764
+ "• <b>AI</b> (orange ✓): Send glossary terms to LLM with every translation (increases prompt size)"
12754
12765
  )
12755
12766
  help_msg.setWordWrap(True)
12756
12767
  help_msg.setStyleSheet("background-color: #e3f2fd; padding: 8px; border-radius: 4px; color: #1976d2;")
@@ -12772,8 +12783,8 @@ class SupervertalerQt(QMainWindow):
12772
12783
  # Termbase list with table
12773
12784
  termbase_table = QTableWidget()
12774
12785
  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"])
12786
+ termbase_table.setColumnCount(8)
12787
+ termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority", "AI"])
12777
12788
  termbase_table.horizontalHeader().setStretchLastSection(False)
12778
12789
  termbase_table.setColumnWidth(0, 80) # Type (Project/Background)
12779
12790
  termbase_table.setColumnWidth(1, 180) # Name
@@ -12782,6 +12793,7 @@ class SupervertalerQt(QMainWindow):
12782
12793
  termbase_table.setColumnWidth(4, 50) # Read checkbox
12783
12794
  termbase_table.setColumnWidth(5, 50) # Write checkbox
12784
12795
  termbase_table.setColumnWidth(6, 60) # Priority
12796
+ termbase_table.setColumnWidth(7, 40) # AI checkbox
12785
12797
  termbase_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
12786
12798
  termbase_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
12787
12799
  termbase_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Disable inline editing
@@ -13475,7 +13487,43 @@ class SupervertalerQt(QMainWindow):
13475
13487
  priority_item.setToolTip("No priority - glossary not readable")
13476
13488
  priority_item.setFlags(priority_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
13477
13489
  termbase_table.setItem(row, 6, priority_item)
13478
-
13490
+
13491
+ # AI checkbox (purple/orange) - whether to inject terms into LLM prompts
13492
+ ai_enabled = termbase_mgr.get_termbase_ai_inject(tb['id'])
13493
+ ai_checkbox = OrangeCheckmarkCheckBox()
13494
+ ai_checkbox.setChecked(ai_enabled)
13495
+ ai_checkbox.setToolTip("AI: Send glossary terms to LLM with translation prompts")
13496
+
13497
+ def on_ai_toggle(checked, tb_id=tb['id'], tb_name=tb['name']):
13498
+ if checked:
13499
+ # Show warning when enabling
13500
+ from PyQt6.QtWidgets import QMessageBox
13501
+ msg = QMessageBox()
13502
+ msg.setWindowTitle("Enable AI Injection")
13503
+ msg.setText(f"Enable AI injection for '{tb_name}'?")
13504
+ msg.setInformativeText(
13505
+ "When enabled, ALL terms from this glossary will be sent to the LLM "
13506
+ "with every translation request.\n\n"
13507
+ "This helps the AI consistently use your preferred terminology "
13508
+ "throughout the translation.\n\n"
13509
+ "Recommended for small, curated glossaries (< 500 terms)."
13510
+ )
13511
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
13512
+ msg.setDefaultButton(QMessageBox.StandardButton.Yes)
13513
+ if msg.exec() != QMessageBox.StandardButton.Yes:
13514
+ # User cancelled - revert checkbox
13515
+ sender = termbase_table.cellWidget(termbase_table.currentRow(), 7)
13516
+ if sender:
13517
+ sender.blockSignals(True)
13518
+ sender.setChecked(False)
13519
+ sender.blockSignals(False)
13520
+ return
13521
+ termbase_mgr.set_termbase_ai_inject(tb_id, checked)
13522
+ self.log(f"{'✅ Enabled' if checked else '❌ Disabled'} AI injection for glossary: {tb_name}")
13523
+
13524
+ ai_checkbox.toggled.connect(on_ai_toggle)
13525
+ termbase_table.setCellWidget(row, 7, ai_checkbox)
13526
+
13479
13527
  # Update header checkbox states based on current selection
13480
13528
  tb_read_header_checkbox.blockSignals(True)
13481
13529
  tb_write_header_checkbox.blockSignals(True)
@@ -31565,9 +31613,14 @@ class SupervertalerQt(QMainWindow):
31565
31613
 
31566
31614
  # Get source text
31567
31615
  source_text = current_segment.source
31568
-
31616
+
31617
+ # Get glossary terms for AI injection
31618
+ glossary_terms = self.get_ai_inject_glossary_terms()
31619
+
31569
31620
  # Build combined prompt
31570
- combined = self.prompt_manager_qt.build_final_prompt(source_text, source_lang, target_lang)
31621
+ combined = self.prompt_manager_qt.build_final_prompt(
31622
+ source_text, source_lang, target_lang, glossary_terms=glossary_terms
31623
+ )
31571
31624
 
31572
31625
  # Check for figure/image context
31573
31626
  figure_info = ""
@@ -31594,11 +31647,14 @@ class SupervertalerQt(QMainWindow):
31594
31647
  composition_parts.append(f"📏 Total prompt: {len(combined):,} characters")
31595
31648
 
31596
31649
  if self.prompt_manager_qt.library.active_primary_prompt:
31597
- composition_parts.append(f"✓ Primary prompt attached")
31650
+ composition_parts.append(f"✓ Custom prompt attached")
31598
31651
 
31599
31652
  if self.prompt_manager_qt.library.attached_prompts:
31600
31653
  composition_parts.append(f"✓ {len(self.prompt_manager_qt.library.attached_prompts)} additional prompt(s) attached")
31601
-
31654
+
31655
+ if glossary_terms:
31656
+ composition_parts.append(f"📚 {len(glossary_terms)} glossary term(s) injected")
31657
+
31602
31658
  if figure_info:
31603
31659
  composition_parts.append(figure_info)
31604
31660
 
@@ -31633,12 +31689,59 @@ class SupervertalerQt(QMainWindow):
31633
31689
  image_notice.setStyleSheet("padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 4px solid #ff9800;")
31634
31690
  layout.addWidget(image_notice)
31635
31691
 
31636
- # Text editor for preview
31692
+ # Text editor for preview with syntax highlighting
31637
31693
  text_edit = QTextEdit()
31638
- text_edit.setPlainText(combined)
31639
31694
  text_edit.setReadOnly(True)
31640
31695
  text_edit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
31641
31696
  text_edit.setStyleSheet("font-family: 'Consolas', 'Courier New', monospace; font-size: 9pt;")
31697
+
31698
+ # Format the prompt with color highlighting
31699
+ import html
31700
+ import re
31701
+
31702
+ # Escape HTML entities first
31703
+ formatted_html = html.escape(combined)
31704
+
31705
+ # Replace newlines with <br> for HTML
31706
+ formatted_html = formatted_html.replace('\n', '<br>')
31707
+
31708
+ # Make "# SYSTEM PROMPT" bold and red
31709
+ formatted_html = re.sub(
31710
+ r'(#\s*SYSTEM\s*PROMPT)',
31711
+ r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
31712
+ formatted_html,
31713
+ flags=re.IGNORECASE
31714
+ )
31715
+
31716
+ # Make "# CUSTOM PROMPT" bold and red
31717
+ formatted_html = re.sub(
31718
+ r'(#\s*CUSTOM\s*PROMPT)',
31719
+ r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
31720
+ formatted_html,
31721
+ flags=re.IGNORECASE
31722
+ )
31723
+
31724
+ # Make "# GLOSSARY" bold and orange
31725
+ formatted_html = re.sub(
31726
+ r'(#\s*GLOSSARY)',
31727
+ r'<span style="color: #FF9800; font-weight: bold; font-size: 11pt;">\1</span>',
31728
+ formatted_html,
31729
+ flags=re.IGNORECASE
31730
+ )
31731
+
31732
+ # Make source text section blue (pattern: "XX text:<br>..." until double line break or # header)
31733
+ # Match language code + " text:" followed by content until "# " or end
31734
+ formatted_html = re.sub(
31735
+ r'(\w{2,5}\s+text:)(<br>)(.*?)(<br><br>(?:#|\*\*YOUR TRANSLATION)|$)',
31736
+ r'<span style="color: #1565c0; font-weight: bold;">\1</span>\2<span style="color: #1565c0;">\3</span>\4',
31737
+ formatted_html,
31738
+ flags=re.DOTALL
31739
+ )
31740
+
31741
+ # Wrap in pre-like styling div
31742
+ formatted_html = f'<div style="font-family: Consolas, Courier New, monospace; white-space: pre-wrap;">{formatted_html}</div>'
31743
+
31744
+ text_edit.setHtml(formatted_html)
31642
31745
  layout.addWidget(text_edit, 1)
31643
31746
 
31644
31747
  # Close button
@@ -35380,7 +35483,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35380
35483
  primary_prompt_text = f"✅ {prompt_path}"
35381
35484
  attached_count = len(library.attached_prompt_paths) if library.attached_prompt_paths else 0
35382
35485
 
35383
- ai_layout.addWidget(QLabel(f"<b>Primary Prompt:</b> {primary_prompt_text}"))
35486
+ ai_layout.addWidget(QLabel(f"<b>Custom Prompt:</b> {primary_prompt_text}"))
35384
35487
  if attached_count > 0:
35385
35488
  ai_layout.addWidget(QLabel(f"<b>Attached Prompts:</b> {attached_count}"))
35386
35489
 
@@ -40466,13 +40569,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
40466
40569
  except Exception as e:
40467
40570
  self.log(f"⚠ Could not add surrounding segments: {e}")
40468
40571
 
40572
+ # Get glossary terms for AI injection
40573
+ glossary_terms = self.get_ai_inject_glossary_terms()
40574
+
40469
40575
  custom_prompt = self.prompt_manager_qt.build_final_prompt(
40470
40576
  source_text=segment.source,
40471
40577
  source_lang=self.current_project.source_lang,
40472
40578
  target_lang=self.current_project.target_lang,
40473
- mode="single"
40579
+ mode="single",
40580
+ glossary_terms=glossary_terms
40474
40581
  )
40475
-
40582
+
40476
40583
  # Add surrounding context before the translation delimiter
40477
40584
  if surrounding_context:
40478
40585
  # Insert before the "YOUR TRANSLATION" delimiter
@@ -42023,11 +42130,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42023
42130
  if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt and batch_segments:
42024
42131
  try:
42025
42132
  first_segment = batch_segments[0][1]
42133
+ # Get glossary terms for AI injection
42134
+ glossary_terms = self.get_ai_inject_glossary_terms()
42026
42135
  base_prompt = self.prompt_manager_qt.build_final_prompt(
42027
42136
  source_text=first_segment.source,
42028
42137
  source_lang=source_lang,
42029
42138
  target_lang=target_lang,
42030
- mode="single"
42139
+ mode="single",
42140
+ glossary_terms=glossary_terms
42031
42141
  )
42032
42142
  if "**SOURCE TEXT:**" in base_prompt:
42033
42143
  base_prompt = base_prompt.split("**SOURCE TEXT:**")[0].strip()
@@ -42455,11 +42565,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42455
42565
  # Access parent through closure
42456
42566
  parent = self
42457
42567
  if hasattr(parent, 'prompt_manager_qt') and parent.prompt_manager_qt:
42568
+ # Get glossary terms for AI injection
42569
+ glossary_terms = parent.get_ai_inject_glossary_terms() if hasattr(parent, 'get_ai_inject_glossary_terms') else []
42458
42570
  custom_prompt = parent.prompt_manager_qt.build_final_prompt(
42459
42571
  source_text=source_text,
42460
42572
  source_lang=source_lang,
42461
42573
  target_lang=target_lang,
42462
- mode="single"
42574
+ mode="single",
42575
+ glossary_terms=glossary_terms
42463
42576
  )
42464
42577
  except Exception as e:
42465
42578
  self.log(f"⚠ Could not build LLM prompt from manager: {e}")
@@ -42942,7 +43055,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42942
43055
  api_keys['google'] = api_keys['gemini']
42943
43056
 
42944
43057
  return api_keys
42945
-
43058
+
43059
+ def get_ai_inject_glossary_terms(self) -> list:
43060
+ """Get glossary terms from AI-inject-enabled termbases for the current project.
43061
+
43062
+ Returns:
43063
+ List of term dictionaries with source_term, target_term, forbidden keys
43064
+ """
43065
+ if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
43066
+ return []
43067
+
43068
+ project_id = None
43069
+ if hasattr(self, 'current_project') and self.current_project:
43070
+ project_id = getattr(self.current_project, 'id', None)
43071
+
43072
+ return self.termbase_mgr.get_ai_inject_terms(project_id)
43073
+
42946
43074
  def ensure_example_api_keys(self):
42947
43075
  """Create example API keys file on first launch for new users"""
42948
43076
  example_file = self.user_data_path / "api_keys.example.txt"
@@ -48257,7 +48385,87 @@ class BlueCheckmarkCheckBox(QCheckBox):
48257
48385
 
48258
48386
  painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
48259
48387
  painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
48260
-
48388
+
48389
+ painter.end()
48390
+
48391
+
48392
+ class OrangeCheckmarkCheckBox(QCheckBox):
48393
+ """Custom checkbox with orange background and white checkmark when checked (for AI injection)"""
48394
+
48395
+ def __init__(self, text="", parent=None):
48396
+ super().__init__(text, parent)
48397
+ self.setCheckable(True)
48398
+ self.setEnabled(True)
48399
+ self.setStyleSheet("""
48400
+ QCheckBox {
48401
+ font-size: 9pt;
48402
+ spacing: 6px;
48403
+ }
48404
+ QCheckBox::indicator {
48405
+ width: 16px;
48406
+ height: 16px;
48407
+ border: 2px solid #999;
48408
+ border-radius: 3px;
48409
+ background-color: white;
48410
+ }
48411
+ QCheckBox::indicator:checked {
48412
+ background-color: #FF9800;
48413
+ border-color: #FF9800;
48414
+ }
48415
+ QCheckBox::indicator:hover {
48416
+ border-color: #666;
48417
+ }
48418
+ QCheckBox::indicator:checked:hover {
48419
+ background-color: #F57C00;
48420
+ border-color: #F57C00;
48421
+ }
48422
+ """)
48423
+
48424
+ def paintEvent(self, event):
48425
+ """Override paint event to draw white checkmark when checked"""
48426
+ super().paintEvent(event)
48427
+
48428
+ if self.isChecked():
48429
+ from PyQt6.QtWidgets import QStyleOptionButton
48430
+ from PyQt6.QtGui import QPainter, QPen, QColor
48431
+ from PyQt6.QtCore import QPointF
48432
+
48433
+ opt = QStyleOptionButton()
48434
+ self.initStyleOption(opt)
48435
+ indicator_rect = self.style().subElementRect(
48436
+ self.style().SubElement.SE_CheckBoxIndicator,
48437
+ opt,
48438
+ self
48439
+ )
48440
+
48441
+ if indicator_rect.isValid():
48442
+ painter = QPainter(self)
48443
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
48444
+ pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
48445
+ painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
48446
+ painter.setBrush(QColor(255, 255, 255))
48447
+
48448
+ x = indicator_rect.x()
48449
+ y = indicator_rect.y()
48450
+ w = indicator_rect.width()
48451
+ h = indicator_rect.height()
48452
+
48453
+ padding = min(w, h) * 0.15
48454
+ x += padding
48455
+ y += padding
48456
+ w -= padding * 2
48457
+ h -= padding * 2
48458
+
48459
+ check_x1 = x + w * 0.10
48460
+ check_y1 = y + h * 0.50
48461
+ check_x2 = x + w * 0.35
48462
+ check_y2 = y + h * 0.70
48463
+ check_x3 = x + w * 0.90
48464
+ check_y3 = y + h * 0.25
48465
+
48466
+ painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
48467
+ painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
48468
+
48261
48469
  painter.end()
48262
48470
 
48263
48471
 
@@ -186,9 +186,13 @@ def run_all_migrations(db_manager) -> bool:
186
186
  # Migration 3: Add display_order and forbidden fields to synonyms
187
187
  if not migrate_synonym_fields(db_manager):
188
188
  success = False
189
-
189
+
190
+ # Migration 4: Add ai_inject field to termbases
191
+ if not migrate_termbase_ai_inject(db_manager):
192
+ success = False
193
+
190
194
  print("="*60)
191
-
195
+
192
196
  return success
193
197
 
194
198
 
@@ -221,18 +225,26 @@ def check_and_migrate(db_manager) -> bool:
221
225
 
222
226
  # Check if synonyms table exists
223
227
  cursor.execute("""
224
- SELECT name FROM sqlite_master
228
+ SELECT name FROM sqlite_master
225
229
  WHERE type='table' AND name='termbase_synonyms'
226
230
  """)
227
231
  needs_synonyms_table = cursor.fetchone() is None
228
-
232
+
233
+ # Check if termbases table has ai_inject column
234
+ cursor.execute("PRAGMA table_info(termbases)")
235
+ termbase_columns = {row[1] for row in cursor.fetchall()}
236
+ needs_ai_inject = 'ai_inject' not in termbase_columns
237
+
229
238
  if needs_migration:
230
239
  print(f"⚠️ Migration needed - missing columns: {', '.join([c for c in ['project', 'client', 'term_uuid', 'note'] if c not in columns])}")
231
-
240
+
232
241
  if needs_synonyms_table:
233
242
  print("⚠️ Migration needed - termbase_synonyms table missing")
234
-
235
- if needs_migration or needs_synonyms_table:
243
+
244
+ if needs_ai_inject:
245
+ print("⚠️ Migration needed - termbases.ai_inject column missing")
246
+
247
+ if needs_migration or needs_synonyms_table or needs_ai_inject:
236
248
  success = run_all_migrations(db_manager)
237
249
  if success:
238
250
  # Generate UUIDs for terms that don't have them
@@ -316,6 +328,41 @@ def migrate_synonym_fields(db_manager) -> bool:
316
328
  return False
317
329
 
318
330
 
331
+ def migrate_termbase_ai_inject(db_manager) -> bool:
332
+ """
333
+ Add ai_inject column to termbases table.
334
+ When enabled, the termbase's terms will be injected into LLM translation prompts.
335
+
336
+ Args:
337
+ db_manager: DatabaseManager instance
338
+
339
+ Returns:
340
+ True if migration successful
341
+ """
342
+ try:
343
+ cursor = db_manager.cursor
344
+
345
+ # Check which columns exist
346
+ cursor.execute("PRAGMA table_info(termbases)")
347
+ columns = {row[1] for row in cursor.fetchall()}
348
+
349
+ if 'ai_inject' not in columns:
350
+ print("📊 Adding 'ai_inject' column to termbases...")
351
+ cursor.execute("ALTER TABLE termbases ADD COLUMN ai_inject BOOLEAN DEFAULT 0")
352
+ db_manager.connection.commit()
353
+ print(" ✓ Column 'ai_inject' added successfully")
354
+ else:
355
+ print("✅ termbases.ai_inject column already exists")
356
+
357
+ return True
358
+
359
+ except Exception as e:
360
+ print(f"❌ ai_inject migration failed: {e}")
361
+ import traceback
362
+ traceback.print_exc()
363
+ return False
364
+
365
+
319
366
  def generate_missing_uuids(db_manager) -> bool:
320
367
  """
321
368
  Generate UUIDs for any termbase terms that don't have them.
@@ -409,7 +409,111 @@ class TermbaseManager:
409
409
  except Exception as e:
410
410
  self.log(f"✗ Error setting termbase read_only: {e}")
411
411
  return False
412
-
412
+
413
+ def get_termbase_ai_inject(self, termbase_id: int) -> bool:
414
+ """Get whether termbase terms should be injected into LLM prompts"""
415
+ try:
416
+ cursor = self.db_manager.cursor
417
+ cursor.execute("SELECT ai_inject FROM termbases WHERE id = ?", (termbase_id,))
418
+ result = cursor.fetchone()
419
+ return bool(result[0]) if result and result[0] else False
420
+ except Exception as e:
421
+ self.log(f"✗ Error getting termbase ai_inject: {e}")
422
+ return False
423
+
424
+ def set_termbase_ai_inject(self, termbase_id: int, ai_inject: bool) -> bool:
425
+ """Set whether termbase terms should be injected into LLM prompts"""
426
+ try:
427
+ cursor = self.db_manager.cursor
428
+ cursor.execute("""
429
+ UPDATE termbases SET ai_inject = ? WHERE id = ?
430
+ """, (1 if ai_inject else 0, termbase_id))
431
+ self.db_manager.connection.commit()
432
+ status = "enabled" if ai_inject else "disabled"
433
+ self.log(f"✓ AI injection {status} for termbase {termbase_id}")
434
+ return True
435
+ except Exception as e:
436
+ self.log(f"✗ Error setting termbase ai_inject: {e}")
437
+ return False
438
+
439
+ def get_ai_inject_termbases(self, project_id: Optional[int] = None) -> List[Dict]:
440
+ """
441
+ Get all termbases with ai_inject enabled that are active for the given project.
442
+
443
+ Args:
444
+ project_id: Project ID (0 or None for global)
445
+
446
+ Returns:
447
+ List of termbase dictionaries with all terms
448
+ """
449
+ try:
450
+ cursor = self.db_manager.cursor
451
+ proj_id = project_id if project_id else 0
452
+
453
+ cursor.execute("""
454
+ SELECT t.id, t.name, t.source_lang, t.target_lang
455
+ FROM termbases t
456
+ LEFT JOIN termbase_activation ta ON t.id = ta.termbase_id AND ta.project_id = ?
457
+ WHERE t.ai_inject = 1
458
+ AND (ta.is_active = 1 OR (t.is_global = 1 AND ta.is_active IS NULL))
459
+ ORDER BY ta.priority ASC, t.name ASC
460
+ """, (proj_id,))
461
+
462
+ termbases = []
463
+ for row in cursor.fetchall():
464
+ termbases.append({
465
+ 'id': row[0],
466
+ 'name': row[1],
467
+ 'source_lang': row[2],
468
+ 'target_lang': row[3]
469
+ })
470
+ return termbases
471
+ except Exception as e:
472
+ self.log(f"✗ Error getting AI inject termbases: {e}")
473
+ return []
474
+
475
+ def get_ai_inject_terms(self, project_id: Optional[int] = None) -> List[Dict]:
476
+ """
477
+ Get all terms from AI-inject-enabled termbases for the given project.
478
+
479
+ Args:
480
+ project_id: Project ID (0 or None for global)
481
+
482
+ Returns:
483
+ List of term dictionaries with source_term, target_term, forbidden, termbase_name
484
+ """
485
+ try:
486
+ # First get all AI-inject termbases
487
+ ai_termbases = self.get_ai_inject_termbases(project_id)
488
+ if not ai_termbases:
489
+ return []
490
+
491
+ all_terms = []
492
+ cursor = self.db_manager.cursor
493
+
494
+ for tb in ai_termbases:
495
+ cursor.execute("""
496
+ SELECT source_term, target_term, forbidden, priority
497
+ FROM termbase_terms
498
+ WHERE termbase_id = ?
499
+ ORDER BY priority ASC, source_term ASC
500
+ """, (tb['id'],))
501
+
502
+ for row in cursor.fetchall():
503
+ all_terms.append({
504
+ 'source_term': row[0],
505
+ 'target_term': row[1],
506
+ 'forbidden': bool(row[2]) if row[2] else False,
507
+ 'priority': row[3] or 99,
508
+ 'termbase_name': tb['name']
509
+ })
510
+
511
+ self.log(f"📚 Retrieved {len(all_terms)} terms from {len(ai_termbases)} AI-inject glossar{'y' if len(ai_termbases) == 1 else 'ies'}")
512
+ return all_terms
513
+ except Exception as e:
514
+ self.log(f"✗ Error getting AI inject terms: {e}")
515
+ return []
516
+
413
517
  def set_termbase_priority(self, termbase_id: int, project_id: int, priority: int) -> bool:
414
518
  """
415
519
  Set manual priority for a termbase in a specific project.
@@ -367,7 +367,7 @@ class UnifiedPromptLibrary:
367
367
 
368
368
  self.active_primary_prompt = self.prompts[relative_path]['content']
369
369
  self.active_primary_prompt_path = relative_path
370
- self.log(f"✓ Set primary prompt: {self.prompts[relative_path].get('name', relative_path)}")
370
+ self.log(f"✓ Set custom prompt: {self.prompts[relative_path].get('name', relative_path)}")
371
371
  return True
372
372
 
373
373
  def set_external_primary_prompt(self, file_path: str) -> Tuple[bool, str]:
@@ -399,7 +399,7 @@ class UnifiedPromptLibrary:
399
399
  self.active_primary_prompt = content
400
400
  self.active_primary_prompt_path = f"[EXTERNAL] {file_path}"
401
401
 
402
- self.log(f"✓ Set external primary prompt: {display_name}")
402
+ self.log(f"✓ Set external custom prompt: {display_name}")
403
403
  return True, display_name
404
404
 
405
405
  def attach_prompt(self, relative_path: str) -> bool: