supervertaler 1.9.181__py3-none-any.whl → 1.9.184__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.

Potentially problematic release.


This version of supervertaler might be problematic. Click here for more details.

modules/project_tm.py ADDED
@@ -0,0 +1,320 @@
1
+ """
2
+ ProjectTM - In-memory TM for instant grid lookups (Total Recall architecture)
3
+
4
+ This module implements a lightweight in-memory Translation Memory that extracts
5
+ relevant segments from the full TM database on project load. This makes grid
6
+ navigation instant while keeping the full TM for concordance searches.
7
+
8
+ Inspired by CafeTran's "Total Recall" feature.
9
+ """
10
+
11
+ import sqlite3
12
+ import threading
13
+ from difflib import SequenceMatcher
14
+ from typing import Dict, List, Optional, Callable
15
+ import re
16
+
17
+
18
+ class ProjectTM:
19
+ """
20
+ Lightweight in-memory TM extracted from the main TM database.
21
+
22
+ On project load, extracts segments that are relevant to the current project
23
+ (fuzzy matches above threshold) into an in-memory SQLite database for
24
+ instant lookups during grid navigation.
25
+
26
+ Usage:
27
+ project_tm = ProjectTM()
28
+ project_tm.extract_from_database(
29
+ db_manager,
30
+ project_segments,
31
+ tm_ids=['tm1', 'tm2'],
32
+ threshold=0.75,
33
+ progress_callback=lambda cur, total: print(f"{cur}/{total}")
34
+ )
35
+
36
+ # Fast lookup during grid navigation
37
+ matches = project_tm.search("source text to translate")
38
+ """
39
+
40
+ def __init__(self):
41
+ """Initialize in-memory SQLite database for ProjectTM"""
42
+ self.conn = sqlite3.connect(":memory:", check_same_thread=False)
43
+ self.conn.row_factory = sqlite3.Row
44
+ self.lock = threading.Lock()
45
+ self.is_built = False
46
+ self.segment_count = 0
47
+
48
+ # Create the schema
49
+ self._create_schema()
50
+
51
+ def _create_schema(self):
52
+ """Create the in-memory database schema"""
53
+ with self.lock:
54
+ cursor = self.conn.cursor()
55
+ cursor.execute("""
56
+ CREATE TABLE IF NOT EXISTS segments (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ source_text TEXT NOT NULL,
59
+ target_text TEXT NOT NULL,
60
+ source_lower TEXT NOT NULL,
61
+ tm_id TEXT,
62
+ tm_name TEXT,
63
+ similarity REAL,
64
+ original_id INTEGER
65
+ )
66
+ """)
67
+ # Index for fast exact match lookups
68
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_source_lower ON segments(source_lower)")
69
+ # FTS5 for fuzzy text search
70
+ cursor.execute("""
71
+ CREATE VIRTUAL TABLE IF NOT EXISTS segments_fts USING fts5(
72
+ source_text,
73
+ content=segments,
74
+ content_rowid=id
75
+ )
76
+ """)
77
+ self.conn.commit()
78
+
79
+ def clear(self):
80
+ """Clear all segments from the ProjectTM"""
81
+ with self.lock:
82
+ cursor = self.conn.cursor()
83
+ cursor.execute("DELETE FROM segments")
84
+ cursor.execute("DELETE FROM segments_fts")
85
+ self.conn.commit()
86
+ self.is_built = False
87
+ self.segment_count = 0
88
+
89
+ def extract_from_database(
90
+ self,
91
+ db_manager,
92
+ project_segments: List,
93
+ tm_ids: List[str] = None,
94
+ source_lang: str = None,
95
+ target_lang: str = None,
96
+ threshold: float = 0.75,
97
+ progress_callback: Optional[Callable[[int, int], None]] = None,
98
+ log_callback: Optional[Callable[[str], None]] = None
99
+ ) -> int:
100
+ """
101
+ Extract relevant segments from the main TM database into ProjectTM.
102
+
103
+ For each unique source text in the project, searches the TM for fuzzy
104
+ matches above the threshold and stores them in memory.
105
+
106
+ Args:
107
+ db_manager: The main database manager with TM data
108
+ project_segments: List of project segments to find matches for
109
+ tm_ids: List of TM IDs to search (None = all active TMs)
110
+ source_lang: Source language filter
111
+ target_lang: Target language filter
112
+ threshold: Minimum similarity threshold (0.0-1.0)
113
+ progress_callback: Optional callback(current, total) for progress
114
+ log_callback: Optional callback(message) for logging
115
+
116
+ Returns:
117
+ Number of TM segments extracted
118
+ """
119
+ def log(msg):
120
+ if log_callback:
121
+ log_callback(msg)
122
+ else:
123
+ print(msg)
124
+
125
+ self.clear()
126
+
127
+ if not project_segments or not db_manager:
128
+ log(f"[ProjectTM] Early exit: segments={bool(project_segments)}, db={bool(db_manager)}")
129
+ return 0
130
+
131
+ # Get unique source texts from project
132
+ unique_sources = {}
133
+ for seg in project_segments:
134
+ # Try both 'source' and 'source_text' attributes (different segment types use different names)
135
+ source = getattr(seg, 'source', None) or getattr(seg, 'source_text', None)
136
+ if source and source.strip():
137
+ # Normalize: strip and lowercase for deduplication
138
+ key = source.strip().lower()
139
+ if key not in unique_sources:
140
+ unique_sources[key] = source.strip()
141
+
142
+ total = len(unique_sources)
143
+ log(f"[ProjectTM] Found {total} unique source texts from {len(project_segments)} segments")
144
+ if total == 0:
145
+ return 0
146
+
147
+ extracted_count = 0
148
+ seen_sources = set() # Deduplicate TM entries
149
+
150
+ cursor = self.conn.cursor()
151
+
152
+ log(f"[ProjectTM] Searching TMs: {tm_ids}, threshold={threshold}, langs={source_lang}->{target_lang}")
153
+
154
+ for i, (key, source_text) in enumerate(unique_sources.items()):
155
+ if progress_callback and i % 10 == 0:
156
+ progress_callback(i, total)
157
+
158
+ try:
159
+ # Search main TM database for fuzzy matches
160
+ matches = db_manager.search_fuzzy_matches(
161
+ source_text,
162
+ tm_ids=tm_ids,
163
+ threshold=threshold,
164
+ max_results=10, # Keep top 10 matches per source
165
+ source_lang=source_lang,
166
+ target_lang=target_lang,
167
+ bidirectional=True
168
+ )
169
+
170
+ # Debug: log first search
171
+ if i == 0:
172
+ log(f"[ProjectTM] First search '{source_text[:50]}...' returned {len(matches)} matches")
173
+
174
+ for match in matches:
175
+ match_source = match.get('source_text', '')
176
+ match_target = match.get('target_text', '')
177
+
178
+ if not match_source or not match_target:
179
+ continue
180
+
181
+ # Deduplicate by source text
182
+ source_key = match_source.strip().lower()
183
+ if source_key in seen_sources:
184
+ continue
185
+ seen_sources.add(source_key)
186
+
187
+ # Insert into ProjectTM
188
+ cursor.execute("""
189
+ INSERT INTO segments (source_text, target_text, source_lower,
190
+ tm_id, tm_name, similarity, original_id)
191
+ VALUES (?, ?, ?, ?, ?, ?, ?)
192
+ """, (
193
+ match_source,
194
+ match_target,
195
+ source_key,
196
+ match.get('tm_id'),
197
+ match.get('tm_name', 'Unknown TM'),
198
+ match.get('similarity', 0),
199
+ match.get('id')
200
+ ))
201
+ extracted_count += 1
202
+
203
+ except Exception as e:
204
+ # Log but continue - don't fail extraction for one bad segment
205
+ pass
206
+
207
+ # Commit all inserts
208
+ self.conn.commit()
209
+
210
+ # Rebuild FTS5 index
211
+ try:
212
+ cursor.execute("INSERT INTO segments_fts(segments_fts) VALUES('rebuild')")
213
+ self.conn.commit()
214
+ except Exception:
215
+ pass # FTS rebuild may fail if no data, that's OK
216
+
217
+ if progress_callback:
218
+ progress_callback(total, total)
219
+
220
+ self.is_built = True
221
+ self.segment_count = extracted_count
222
+
223
+ return extracted_count
224
+
225
+ def search(self, source_text: str, max_results: int = 5) -> List[Dict]:
226
+ """
227
+ Search ProjectTM for matches (instant lookup).
228
+
229
+ First checks for exact matches, then falls back to fuzzy search.
230
+
231
+ Args:
232
+ source_text: Source text to search for
233
+ max_results: Maximum number of results to return
234
+
235
+ Returns:
236
+ List of match dictionaries with source_text, target_text, similarity, etc.
237
+ """
238
+ if not self.is_built or not source_text:
239
+ return []
240
+
241
+ source_lower = source_text.strip().lower()
242
+ results = []
243
+
244
+ with self.lock:
245
+ cursor = self.conn.cursor()
246
+
247
+ # 1. Check for exact match first (fastest)
248
+ cursor.execute("""
249
+ SELECT * FROM segments WHERE source_lower = ? LIMIT 1
250
+ """, (source_lower,))
251
+ exact = cursor.fetchone()
252
+
253
+ if exact:
254
+ results.append({
255
+ 'source_text': exact['source_text'],
256
+ 'target_text': exact['target_text'],
257
+ 'tm_id': exact['tm_id'],
258
+ 'tm_name': exact['tm_name'],
259
+ 'similarity': 1.0, # Exact match
260
+ 'match_pct': 100,
261
+ 'id': exact['original_id']
262
+ })
263
+ return results # Exact match - no need to search further
264
+
265
+ # 2. FTS5 fuzzy search
266
+ try:
267
+ # Tokenize query for FTS5
268
+ clean_text = re.sub(r'[^\w\s]', ' ', source_text)
269
+ search_terms = [t for t in clean_text.split() if len(t) > 2]
270
+
271
+ if search_terms:
272
+ fts_query = ' OR '.join(f'"{term}"' for term in search_terms[:10])
273
+
274
+ cursor.execute("""
275
+ SELECT s.*, bm25(segments_fts) as rank
276
+ FROM segments s
277
+ JOIN segments_fts ON s.id = segments_fts.rowid
278
+ WHERE segments_fts MATCH ?
279
+ ORDER BY rank
280
+ LIMIT ?
281
+ """, (fts_query, max_results * 3)) # Get more candidates for re-ranking
282
+
283
+ candidates = cursor.fetchall()
284
+
285
+ # Re-rank by actual similarity
286
+ for row in candidates:
287
+ similarity = self._calculate_similarity(source_text, row['source_text'])
288
+ if similarity >= 0.5: # Lower threshold for ProjectTM (pre-filtered)
289
+ results.append({
290
+ 'source_text': row['source_text'],
291
+ 'target_text': row['target_text'],
292
+ 'tm_id': row['tm_id'],
293
+ 'tm_name': row['tm_name'],
294
+ 'similarity': similarity,
295
+ 'match_pct': int(similarity * 100),
296
+ 'id': row['original_id']
297
+ })
298
+
299
+ # Sort by similarity and limit
300
+ results.sort(key=lambda x: x['similarity'], reverse=True)
301
+ results = results[:max_results]
302
+
303
+ except Exception:
304
+ pass # FTS search may fail, return what we have
305
+
306
+ return results
307
+
308
+ def _calculate_similarity(self, text1: str, text2: str) -> float:
309
+ """Calculate similarity ratio between two texts"""
310
+ # Strip HTML/XML tags for comparison
311
+ clean1 = re.sub(r'<[^>]+>', '', text1).lower()
312
+ clean2 = re.sub(r'<[^>]+>', '', text2).lower()
313
+ return SequenceMatcher(None, clean1, clean2).ratio()
314
+
315
+ def get_stats(self) -> Dict:
316
+ """Get statistics about the ProjectTM"""
317
+ return {
318
+ 'is_built': self.is_built,
319
+ 'segment_count': self.segment_count
320
+ }
@@ -609,7 +609,6 @@ class TermbaseManager:
609
609
  """, (project_id,))
610
610
 
611
611
  active_ids = [row[0] for row in cursor.fetchall()]
612
- self.log(f"📋 Found {len(active_ids)} active termbases for project {project_id}: {active_ids}")
613
612
  return active_ids
614
613
  except Exception as e:
615
614
  self.log(f"✗ Error getting active termbase IDs: {e}")
@@ -172,7 +172,7 @@ class TermBlock(QWidget):
172
172
  # Get theme colors
173
173
  is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
174
174
  separator_color = "#555555" if is_dark else "#CCCCCC"
175
- source_text_color = "#E0E0E0" if is_dark else "#333"
175
+ source_text_color = "#FFFFFF" if is_dark else "#333"
176
176
  no_match_color = "#666666" if is_dark else "#ddd"
177
177
  no_match_bg = "#2A2A2A" if is_dark else "#F5F5F5"
178
178
 
@@ -224,10 +224,17 @@ class TermBlock(QWidget):
224
224
  if self.translations:
225
225
  target_text = primary_translation.get('target_term', primary_translation.get('target', ''))
226
226
  termbase_name = primary_translation.get('termbase_name', '')
227
-
228
- # Background color based on termbase type
229
- bg_color = "#FFE5F0" if self.is_effective_project else "#D6EBFF" # Pink for project, light blue for regular
230
- hover_color = "#FFD0E8" if self.is_effective_project else "#BBDEFB" # Slightly darker on hover
227
+
228
+ # Background color based on termbase type (theme-aware)
229
+ is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
230
+ if is_dark:
231
+ # Dark mode: darker backgrounds
232
+ bg_color = "#4A2D3A" if self.is_effective_project else "#2D3E4A" # Dark pink/blue
233
+ hover_color = "#5A3D4A" if self.is_effective_project else "#3D4E5A" # Lighter on hover
234
+ else:
235
+ # Light mode: original colors
236
+ bg_color = "#FFE5F0" if self.is_effective_project else "#D6EBFF" # Pink for project, light blue for regular
237
+ hover_color = "#FFD0E8" if self.is_effective_project else "#BBDEFB" # Slightly darker on hover
231
238
 
232
239
  # Create horizontal layout for target + shortcut badge
233
240
  # Apply background to container so it covers both text and badge
@@ -251,9 +258,11 @@ class TermBlock(QWidget):
251
258
  target_font.setBold(self.font_bold)
252
259
  target_label.setFont(target_font)
253
260
  target_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
261
+ # Theme-aware text color
262
+ target_text_color = "#B0C4DE" if is_dark else "#0052A3" # Light blue in dark mode
254
263
  target_label.setStyleSheet(f"""
255
264
  QLabel {{
256
- color: #0052A3;
265
+ color: {target_text_color};
257
266
  padding: 0px;
258
267
  background-color: transparent;
259
268
  border: none;
@@ -312,11 +321,12 @@ class TermBlock(QWidget):
312
321
  if len(self.translations) > 1:
313
322
  count_label = QLabel(f"+{len(self.translations) - 1}")
314
323
  count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
315
- count_label.setStyleSheet("""
316
- QLabel {
317
- color: #999;
324
+ count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
325
+ count_label.setStyleSheet(f"""
326
+ QLabel {{
327
+ color: {count_color};
318
328
  font-size: 7px;
319
- }
329
+ }}
320
330
  """)
321
331
  layout.addWidget(count_label)
322
332
  return
@@ -336,10 +346,13 @@ class TermBlock(QWidget):
336
346
  badge_label = QLabel(badge_text)
337
347
  badge_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
338
348
  badge_label.setFixedSize(badge_width, 14)
349
+ # Theme-aware badge colors
350
+ badge_bg = "#4A90E2" if is_dark else "#1976D2" # Lighter blue in dark mode
351
+ badge_text_color = "#FFFFFF" if is_dark else "white"
339
352
  badge_label.setStyleSheet(f"""
340
353
  QLabel {{
341
- background-color: #1976D2;
342
- color: white;
354
+ background-color: {badge_bg};
355
+ color: {badge_text_color};
343
356
  font-size: 9px;
344
357
  font-weight: bold;
345
358
  border-radius: 7px;
@@ -352,16 +365,17 @@ class TermBlock(QWidget):
352
365
  target_layout.addWidget(badge_label)
353
366
 
354
367
  layout.addWidget(target_container)
355
-
368
+
356
369
  # Show count if multiple translations - very compact
357
370
  if len(self.translations) > 1:
358
371
  count_label = QLabel(f"+{len(self.translations) - 1}")
359
372
  count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
360
- count_label.setStyleSheet("""
361
- QLabel {
362
- color: #999;
373
+ count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
374
+ count_label.setStyleSheet(f"""
375
+ QLabel {{
376
+ color: {count_color};
363
377
  font-size: 7px;
364
- }
378
+ }}
365
379
  """)
366
380
  layout.addWidget(count_label)
367
381
  else:
@@ -439,7 +453,7 @@ class NTBlock(QWidget):
439
453
 
440
454
  # Get theme colors
441
455
  is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
442
- source_text_color = "#E0E0E0" if is_dark else "#5D4E37"
456
+ source_text_color = "#FFFFFF" if is_dark else "#5D4E37"
443
457
 
444
458
  # Pastel yellow border for non-translatables
445
459
  border_color = "#E6C200" # Darker yellow for border
@@ -637,6 +651,17 @@ class TermviewWidget(QWidget):
637
651
  is_dark = theme.name == "Dark"
638
652
  info_label_color = "#909090" if is_dark else info_text
639
653
  self.info_label.setStyleSheet(f"color: {info_label_color}; font-size: 10px; padding: 5px;")
654
+
655
+ # Refresh term blocks to pick up new theme colors
656
+ if hasattr(self, '_last_termbase_matches') and hasattr(self, '_last_nt_matches') and hasattr(self, 'current_source'):
657
+ # Re-render with stored matches to apply new theme colors
658
+ if self.current_source:
659
+ self.update_with_matches(
660
+ self.current_source,
661
+ self._last_termbase_matches or [],
662
+ self._last_nt_matches,
663
+ self._status_hint if hasattr(self, '_status_hint') else None
664
+ )
640
665
 
641
666
  def set_font_settings(self, font_family: str = "Segoe UI", font_size: int = 10, bold: bool = False):
642
667
  """Update font settings for Termview
@@ -707,6 +732,9 @@ class TermviewWidget(QWidget):
707
732
  status_hint: Optional hint about why there might be no matches (e.g., 'no_termbases_activated', 'wrong_language')
708
733
  """
709
734
  self.current_source = source_text
735
+ # Store matches for theme refresh
736
+ self._last_termbase_matches = termbase_matches
737
+ self._last_nt_matches = nt_matches
710
738
 
711
739
  # Clear existing blocks and shortcut mappings
712
740
  self.clear_terms()
@@ -734,15 +762,9 @@ class TermviewWidget(QWidget):
734
762
  has_termbase = termbase_matches and len(termbase_matches) > 0
735
763
  has_nt = nt_matches and len(nt_matches) > 0
736
764
 
737
- if not has_termbase and not has_nt:
738
- # Show appropriate message based on status hint
739
- if status_hint == 'no_termbases_activated':
740
- self.info_label.setText("No glossaries activated. Go to Resources → Glossary to activate.")
741
- elif status_hint == 'wrong_language':
742
- self.info_label.setText("Activated glossaries don't match project language pair.")
743
- else:
744
- self.info_label.setText("No terminology or NT matches for this segment")
745
- return
765
+ # Store status hint for info label (will be set at the end)
766
+ self._status_hint = status_hint
767
+ self._has_any_matches = has_termbase or has_nt
746
768
 
747
769
  # Convert termbase matches to dict for easy lookup: {source_term.lower(): [translations]}
748
770
  matches_dict = {}
@@ -874,11 +896,18 @@ class TermviewWidget(QWidget):
874
896
  info_parts.append(f"{blocks_with_translations} terms")
875
897
  if blocks_with_nt > 0:
876
898
  info_parts.append(f"{blocks_with_nt} NTs")
877
-
899
+
878
900
  if info_parts:
879
901
  self.info_label.setText(f"✓ Found {', '.join(info_parts)} in {len(tokens)} words")
880
902
  else:
881
- self.info_label.setText(f"No matches in {len(tokens)} words")
903
+ # Show appropriate message based on status hint when no matches
904
+ status_hint = getattr(self, '_status_hint', None)
905
+ if status_hint == 'no_termbases_activated':
906
+ self.info_label.setText(f"No glossaries activated ({len(tokens)} words)")
907
+ elif status_hint == 'wrong_language':
908
+ self.info_label.setText(f"Glossaries don't match language pair ({len(tokens)} words)")
909
+ else:
910
+ self.info_label.setText(f"No matches in {len(tokens)} words")
882
911
 
883
912
  def get_all_termbase_matches(self, text: str) -> Dict[str, List[Dict]]:
884
913
  """
@@ -205,20 +205,14 @@ class TMDatabase:
205
205
  Returns:
206
206
  List of match dictionaries sorted by similarity
207
207
  """
208
- print(f"[DEBUG] TMDatabase.search_all: source='{source[:50]}...', tm_ids={tm_ids}")
209
-
210
208
  # Determine which TMs to search
211
209
  # If tm_ids is None or empty, search ALL TMs (don't filter by tm_id)
212
210
  if tm_ids is None and enabled_only:
213
211
  tm_ids = [tm_id for tm_id, meta in self.tm_metadata.items() if meta.get('enabled', True)]
214
- print(f"[DEBUG] TMDatabase.search_all: No tm_ids provided, using from metadata: {tm_ids}")
215
-
212
+
216
213
  # If tm_ids is still empty, set to None to search ALL TMs
217
214
  if tm_ids is not None and len(tm_ids) == 0:
218
215
  tm_ids = None
219
- print(f"[DEBUG] TMDatabase.search_all: Empty tm_ids, setting to None to search ALL")
220
-
221
- print(f"[DEBUG] TMDatabase.search_all: Final tm_ids to search: {tm_ids}")
222
216
 
223
217
  # First try exact match
224
218
  exact_match = self.db.get_exact_match(
@@ -227,8 +221,7 @@ class TMDatabase:
227
221
  source_lang=self.source_lang,
228
222
  target_lang=self.target_lang
229
223
  )
230
- print(f"[DEBUG] TMDatabase.search_all: Exact match result: {exact_match}")
231
-
224
+
232
225
  if exact_match:
233
226
  # Format as match dictionary
234
227
  return [{
@@ -241,7 +234,6 @@ class TMDatabase:
241
234
  }]
242
235
 
243
236
  # Try fuzzy matches
244
- print(f"[DEBUG] TMDatabase.search_all: Calling fuzzy search with source_lang={self.source_lang}, target_lang={self.target_lang}")
245
237
  fuzzy_matches = self.db.search_fuzzy_matches(
246
238
  source=source,
247
239
  tm_ids=tm_ids,
@@ -250,8 +242,7 @@ class TMDatabase:
250
242
  source_lang=self.source_lang,
251
243
  target_lang=self.target_lang
252
244
  )
253
- print(f"[DEBUG] TMDatabase.search_all: Fuzzy search returned {len(fuzzy_matches)} matches")
254
-
245
+
255
246
  # Format matches for UI
256
247
  formatted_matches = []
257
248
  for match in fuzzy_matches:
@@ -1676,13 +1676,6 @@ class TranslationResultsPanel(QWidget):
1676
1676
  Args:
1677
1677
  matches_dict: Dict with keys like "NT", "MT", "TM", "Termbases"
1678
1678
  """
1679
- print(f"🎯 TranslationResultsPanel.set_matches() called with matches_dict keys: {list(matches_dict.keys())}")
1680
- for match_type, matches in matches_dict.items():
1681
- print(f" {match_type}: {len(matches)} matches")
1682
- if match_type == "Termbases" and matches:
1683
- for i, match in enumerate(matches[:2]): # Show first 2 for debugging
1684
- print(f" [{i}] {match.source} → {match.target}")
1685
-
1686
1679
  # Ensure CompactMatchItem has current theme_manager
1687
1680
  if self.theme_manager:
1688
1681
  CompactMatchItem.theme_manager = self.theme_manager
@@ -2372,8 +2372,20 @@ class UnifiedPromptManagerQt:
2372
2372
  else:
2373
2373
  # Name unchanged, just update in place
2374
2374
  if self.library.save_prompt(path, prompt_data):
2375
+ # Refresh active prompts if this prompt is currently active or attached
2376
+ # This ensures "Preview Combined" shows the updated content immediately
2377
+ if self.library.active_primary_prompt_path == path:
2378
+ # Update cached primary prompt content
2379
+ self.library.active_primary_prompt = self.library.prompts[path]['content']
2380
+
2381
+ if path in self.library.attached_prompt_paths:
2382
+ # Update cached attached prompt content
2383
+ idx = self.library.attached_prompt_paths.index(path)
2384
+ self.library.attached_prompts[idx] = self.library.prompts[path]['content']
2385
+
2375
2386
  QMessageBox.information(self.main_widget, "Saved", "Prompt updated successfully!")
2376
2387
  self._refresh_tree()
2388
+ self._update_attached_list() # Refresh attached list to show updated names
2377
2389
  else:
2378
2390
  QMessageBox.warning(self.main_widget, "Error", "Failed to save prompt")
2379
2391
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supervertaler
3
- Version: 1.9.181
3
+ Version: 1.9.184
4
4
  Summary: Professional AI-enhanced 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
@@ -1,4 +1,4 @@
1
- Supervertaler.py,sha256=8dvcmMQ-U7MpzzXQVLAsoB0O-yF_HMl142HfIiaMprE,2333467
1
+ Supervertaler.py,sha256=J2_iqhhPh9GFGeJxEZkGeZs-nxoB7OZAfaujHjVsF8s,2342058
2
2
  modules/__init__.py,sha256=G58XleS-EJ2sX4Kehm-3N2m618_W2Es0Kg8CW_eBG7g,327
3
3
  modules/ai_actions.py,sha256=i5MJcM-7Y6CAvKUwxmxrVHeoZAVtAP7aRDdWM5KLkO0,33877
4
4
  modules/ai_attachment_manager.py,sha256=juZlrW3UPkIkcnj0SREgOQkQROLf0fcu3ShZcKXMxsI,11361
@@ -14,6 +14,7 @@ modules/docx_handler.py,sha256=jSlZs5tollJnsnIA80buEXLfZBunp_GQ9lCtFZPUnBs,34053
14
14
  modules/encoding_repair.py,sha256=mrQSOMW-tvaowLja_gtXDJv3Qw4YhLCLsuZtBS0BVms,11900
15
15
  modules/encoding_repair_Qt.py,sha256=UosUOpMnXf0gkgp_F_4wszd25Op_3X_4h9kWMomK0Sk,17533
16
16
  modules/encoding_repair_ui.py,sha256=bRnyFg-5_Lz-bZoNT5eivM5LKsQgpoxbllsffYW3fAg,18619
17
+ modules/extract_tm.py,sha256=ix58ti1_Zkd2dxIx1PwN8rWxmYHGv2zsUPrT-VfcMwA,18228
17
18
  modules/feature_manager.py,sha256=toQnOjeCPMTWp3sas-J88-ZAomFdoeRLKPc19C-6XFo,12416
18
19
  modules/figure_context_manager.py,sha256=hy7h5PcvN8qr8nQtDS41YvTwr6gecWvmjzMBP43_LM8,12682
19
20
  modules/file_dialog_helper.py,sha256=0dthKAFBkoQkdGm_zsYQMImn31Z9Bc9-VKNV5idZtkQ,3767
@@ -34,6 +35,7 @@ modules/pdf_rescue_Qt.py,sha256=9W_M0Zms4miapQbrqm-viHNCpaW39GL9VaKKFCJxpnE,8047
34
35
  modules/pdf_rescue_tkinter.py,sha256=a4R_OUnn7X5O_XMR1roybrdu1aXoGCwwO-mwYB2ZpOg,39606
35
36
  modules/phrase_docx_handler.py,sha256=7vJNbvxxURzdcinZ3rkqyJ-7Y5O1NpVL4Lvu9NuGFjQ,18598
36
37
  modules/project_home_panel.py,sha256=P0PgMnoPp6WEiGrfq8cNJNEdxO83aHQDdXzRLqF173w,6810
38
+ modules/project_tm.py,sha256=TQUc9ApZjfiKZlA4bc1PrHwtEtk_XM9XArCd53De_20,12327
37
39
  modules/prompt_assistant.py,sha256=shkZqNTvyQKNDO_9aFEu1_gN0zQq0fR5krXkWfnTR2Y,13150
38
40
  modules/prompt_library.py,sha256=t5w4cqB6_Sin4BQDVNALKpfB1EN_oaDeHFwlHxILLSY,26894
39
41
  modules/prompt_library_migration.py,sha256=fv3RHhe2-EnH50XW5tyTWy0YP_KJ2EsESuTxR8klfmI,17639
@@ -58,8 +60,8 @@ modules/tag_manager.py,sha256=g66S0JSxdguN9AhWzZG3hsIz87Ul51wQ3c2wOCTZVSk,12789
58
60
  modules/term_extractor.py,sha256=qPvKNCVXFTGEGwXNvvC0cfCmdb5c3WhzE38EOgKdKUI,11253
59
61
  modules/termbase_entry_editor.py,sha256=iWO9CgLjMomGAqBXDsGAX7TFJvDOp2s_taS4gBL1rZY,35818
60
62
  modules/termbase_import_export.py,sha256=16IAY04IS_rgt0GH5UOUzUI5NoqAli4JMfMquxmFBm0,23552
61
- modules/termbase_manager.py,sha256=7KXEFab6y0o1EmFZwHs3ADklC95udVenxvrmN4XUoj0,48808
62
- modules/termview_widget.py,sha256=PibDY55CjBY4BSb6aMGEp4Y2JHNB67QWOQFTDNyeYy8,53968
63
+ modules/termbase_manager.py,sha256=XAVrz-wt8jKcjoD6ocHoXewY5PN0A0GeqFEctsv0jS8,48697
64
+ modules/termview_widget.py,sha256=FsNnSWh86PCnmKcC3fFJS8MJNdVvRpgE5e8-u4jAosY,55742
63
65
  modules/theme_manager.py,sha256=Qk_jfCmfm7fjdMAOyBHpD18w3MiRfWBZk0cHTw6yAAg,18639
64
66
  modules/tm_editor_dialog.py,sha256=AzGwq4QW641uFJdF8DljLTRRp4FLoYX3Pe4rlTjQWNg,3517
65
67
  modules/tm_manager_qt.py,sha256=h2bvXkRuboHf_RRz9-5FX35GVRlpXgRDWeXyj1QWtPs,54406
@@ -69,17 +71,17 @@ modules/tmx_editor_qt.py,sha256=PxBIUw_06PHYTBHsd8hZzVJXW8T0A0ljfz1Wjjsa4yU,1170
69
71
  modules/tmx_generator.py,sha256=pNkxwdMLvSRMMru0lkB1gvViIpg9BQy1EVhRbwoef3k,9426
70
72
  modules/tracked_changes.py,sha256=S_BIEC6r7wVAwjG42aSy_RgH4KaMAC8GS5thEvqrYdE,39480
71
73
  modules/trados_docx_handler.py,sha256=VPRAQ73cUHs_SEj6x81z1PmSxfjnwPBp9P4fXeK3KpQ,16363
72
- modules/translation_memory.py,sha256=13PDK4_kgYrWTACWBIBypOh2DvoxY9cRT8U6ulilbh4,28739
73
- modules/translation_results_panel.py,sha256=DmEe0pZRSfcZFg2cWeEREK7H9vrTcPkgeuMW54Pgrys,92505
74
+ modules/translation_memory.py,sha256=LnG8csZNL2GTHXT4zk0uecJEtvRc-MKwv7Pt7EX3s7s,28002
75
+ modules/translation_results_panel.py,sha256=OWqzV9xmJOi8NGCi3h42nq-qE7-v6WStjQWRsghCVbQ,92044
74
76
  modules/translation_services.py,sha256=lyVpWuZK1wtVtYZMDMdLoq1DHBoSaeAnp-Yejb0TlVQ,10530
75
77
  modules/unified_prompt_library.py,sha256=96u4WlMwnmmhD4uNJHZ-qVQj8v9_8dA2AVCWpBcwTrg,26006
76
- modules/unified_prompt_manager_qt.py,sha256=U89UFGG-M7BLetoaLAlma0x-n8SIyx682DhSvaRnzJs,171285
78
+ modules/unified_prompt_manager_qt.py,sha256=HkGUnH0wlfxt-hVe-nKCeWLyProYdefuuq2slqv8hI4,172160
77
79
  modules/voice_commands.py,sha256=iBb-gjWxRMLhFH7-InSRjYJz1EIDBNA2Pog8V7TtJaY,38516
78
80
  modules/voice_dictation.py,sha256=QmitXfkG-vRt5hIQATjphHdhXfqmwhzcQcbXB6aRzIg,16386
79
81
  modules/voice_dictation_lite.py,sha256=jorY0BmWE-8VczbtGrWwt1zbnOctMoSlWOsQrcufBcc,9423
80
- supervertaler-1.9.181.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
81
- supervertaler-1.9.181.dist-info/METADATA,sha256=OQ1LAlPRGRyBLRAy3jX7II9VFMlrIMqy-s3o1L_NELg,5725
82
- supervertaler-1.9.181.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
83
- supervertaler-1.9.181.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
84
- supervertaler-1.9.181.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
85
- supervertaler-1.9.181.dist-info/RECORD,,
82
+ supervertaler-1.9.184.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
83
+ supervertaler-1.9.184.dist-info/METADATA,sha256=VDzAyylsZ4F4KS2d6gA9EJ-EQO-2oncJpEhgXiEiHoo,5725
84
+ supervertaler-1.9.184.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
85
+ supervertaler-1.9.184.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
86
+ supervertaler-1.9.184.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
87
+ supervertaler-1.9.184.dist-info/RECORD,,