supervertaler 1.9.109__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.
Files changed (85) hide show
  1. Supervertaler.py +44945 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1766 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +904 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +325 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +248 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +333 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1161 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +670 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +656 -0
  76. modules/unified_prompt_manager_qt.py +3715 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.109.dist-info/METADATA +788 -0
  81. supervertaler-1.9.109.dist-info/RECORD +85 -0
  82. supervertaler-1.9.109.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.109.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.109.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.109.dist-info/top_level.txt +2 -0
@@ -0,0 +1,656 @@
1
+ """
2
+ Unified Prompt Library Module
3
+
4
+ Simplified 2-layer architecture:
5
+ 1. System Prompts (in Settings) - mode-specific, auto-selected
6
+ 2. Prompt Library (main UI) - unified workspace with folders, favorites, multi-attach
7
+
8
+ Replaces the old 4-layer system (System/Domain/Project/Style Guides).
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import shutil
14
+ from pathlib import Path
15
+ from datetime import datetime
16
+ from typing import Dict, List, Optional, Tuple
17
+
18
+
19
+ class UnifiedPromptLibrary:
20
+ """
21
+ Manages prompts in a unified library structure with:
22
+ - Nested folder support (unlimited depth)
23
+ - Favorites and Quick Run menu
24
+ - Multi-attach capability
25
+ - Markdown files with YAML frontmatter
26
+ """
27
+
28
+ def __init__(self, library_dir=None, log_callback=None):
29
+ """
30
+ Initialize the Unified Prompt Library.
31
+
32
+ Args:
33
+ library_dir: Path to unified library directory (user_data/Prompt_Library/Library)
34
+ log_callback: Function to call for logging messages
35
+ """
36
+ self.library_dir = Path(library_dir) if library_dir else None
37
+ self.log = log_callback if log_callback else print
38
+
39
+ # Create directory if it doesn't exist
40
+ if self.library_dir:
41
+ self.library_dir.mkdir(parents=True, exist_ok=True)
42
+
43
+ # Prompts storage: {relative_path: prompt_data}
44
+ self.prompts = {}
45
+
46
+ # Active prompt configuration
47
+ self.active_primary_prompt = None # Main prompt
48
+ self.active_primary_prompt_path = None
49
+ self.attached_prompts = [] # List of attached prompt data
50
+ self.attached_prompt_paths = [] # List of paths
51
+
52
+ # Cached lists for quick access
53
+ self._favorites = []
54
+ # Backward-compatible name; now represents QuickMenu (future app-level menu)
55
+ self._quick_run = []
56
+ self._quickmenu_grid = []
57
+
58
+ def set_directory(self, library_dir):
59
+ """Set the library directory after initialization"""
60
+ self.library_dir = Path(library_dir)
61
+ self.library_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ def load_all_prompts(self):
64
+ """Load all prompts from library directory (recursive)"""
65
+ self.prompts = {}
66
+
67
+ if not self.library_dir or not self.library_dir.exists():
68
+ self.log("⚠ Library directory not found")
69
+ return 0
70
+
71
+ count = self._load_from_directory_recursive(self.library_dir, "")
72
+ self.log(f"✓ Loaded {count} prompts from unified library")
73
+
74
+ # Update cached lists
75
+ self._update_favorites_list()
76
+ self._update_quick_run_list()
77
+ self._update_quickmenu_grid_list()
78
+
79
+ return count
80
+
81
+ def _load_from_directory_recursive(self, directory: Path, relative_path: str) -> int:
82
+ """
83
+ Recursively load prompts from directory and subdirectories.
84
+
85
+ Args:
86
+ directory: Absolute path to directory
87
+ relative_path: Relative path from library root (for organization)
88
+
89
+ Returns:
90
+ Number of prompts loaded
91
+ """
92
+ count = 0
93
+
94
+ if not directory.exists():
95
+ return count
96
+
97
+ for item in directory.iterdir():
98
+ # Skip hidden files and __pycache__
99
+ if item.name.startswith('.') or item.name == '__pycache__':
100
+ continue
101
+
102
+ # Recurse into subdirectories
103
+ if item.is_dir():
104
+ sub_relative = str(Path(relative_path) / item.name) if relative_path else item.name
105
+ count += self._load_from_directory_recursive(item, sub_relative)
106
+ continue
107
+
108
+ # Load prompt files (.svprompt is the new format, .md and .txt for legacy)
109
+ if item.suffix.lower() in ['.svprompt', '.md', '.txt']:
110
+ prompt_data = self._parse_markdown(item)
111
+
112
+ if prompt_data:
113
+ # Store with relative path as key
114
+ rel_path = str(Path(relative_path) / item.name) if relative_path else item.name
115
+ prompt_data['_filepath'] = str(item)
116
+ prompt_data['_relative_path'] = rel_path
117
+ prompt_data['_folder'] = relative_path
118
+
119
+ self.prompts[rel_path] = prompt_data
120
+ count += 1
121
+
122
+ return count
123
+
124
+ def _parse_markdown(self, filepath: Path) -> Optional[Dict]:
125
+ """
126
+ Parse Markdown file with YAML frontmatter.
127
+
128
+ Format:
129
+ ---
130
+ name: "Prompt Name"
131
+ description: "Description"
132
+ favorite: false
133
+ quick_run: false
134
+ folder: "Domain Expertise"
135
+ tags: ["medical", "technical"]
136
+ ---
137
+
138
+ # Content
139
+ Actual prompt content here...
140
+ """
141
+ try:
142
+ content = filepath.read_text(encoding='utf-8')
143
+
144
+ # Split frontmatter from content
145
+ if content.startswith('---'):
146
+ content = content[3:].lstrip('\n')
147
+
148
+ if '---' in content:
149
+ frontmatter_str, prompt_content = content.split('---', 1)
150
+ prompt_content = prompt_content.lstrip('\n')
151
+ else:
152
+ self.log(f"⚠ Invalid format in {filepath.name}: closing --- not found")
153
+ return None
154
+ else:
155
+ # No frontmatter - treat entire file as content
156
+ prompt_content = content
157
+ frontmatter_str = ""
158
+
159
+ # Parse YAML frontmatter
160
+ prompt_data = self._parse_yaml(frontmatter_str) if frontmatter_str else {}
161
+
162
+ # Use filename as name if not specified
163
+ if 'name' not in prompt_data:
164
+ prompt_data['name'] = filepath.stem
165
+
166
+ # Store content
167
+ prompt_data['content'] = prompt_content.strip()
168
+
169
+ # Ensure boolean fields exist
170
+ prompt_data.setdefault('favorite', False)
171
+ # Backward compatibility: quick_run is the legacy field; internally we
172
+ # treat it as the "QuickMenu (future app menu)" flag.
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))
176
+ )
177
+ # Keep legacy field in sync so older code/versions still behave.
178
+ prompt_data['quick_run'] = bool(prompt_data['quickmenu_quickmenu'])
179
+
180
+ # New QuickMenu fields
181
+ prompt_data.setdefault('quickmenu_grid', False)
182
+ prompt_data.setdefault('quickmenu_label', prompt_data.get('name', filepath.stem))
183
+ prompt_data.setdefault('tags', [])
184
+
185
+ return prompt_data
186
+
187
+ except Exception as e:
188
+ self.log(f"⚠ Failed to parse {filepath.name}: {e}")
189
+ return None
190
+
191
+ def _parse_yaml(self, yaml_str: str) -> Dict:
192
+ """
193
+ Simple YAML parser for frontmatter.
194
+
195
+ Supports:
196
+ - Simple strings: key: "value" or key: value
197
+ - Booleans: key: true/false
198
+ - Numbers: key: 1.0
199
+ - Arrays: tags: ["item1", "item2"] or tags: [item1, item2]
200
+ """
201
+ data = {}
202
+
203
+ for line in yaml_str.strip().split('\n'):
204
+ line = line.strip()
205
+ if not line or line.startswith('#'):
206
+ continue
207
+
208
+ if ':' not in line:
209
+ continue
210
+
211
+ key, value = line.split(':', 1)
212
+ key = key.strip()
213
+ value = value.strip()
214
+
215
+ # Handle arrays
216
+ if value.startswith('[') and value.endswith(']'):
217
+ # Remove brackets and split by comma
218
+ array_str = value[1:-1]
219
+ items = [item.strip().strip('"').strip("'") for item in array_str.split(',')]
220
+ data[key] = [item for item in items if item] # Filter empty
221
+ continue
222
+
223
+ # Handle booleans
224
+ if value.lower() in ['true', 'false']:
225
+ data[key] = value.lower() == 'true'
226
+ continue
227
+
228
+ # Remove quotes
229
+ if value.startswith('"') and value.endswith('"'):
230
+ value = value[1:-1]
231
+ elif value.startswith("'") and value.endswith("'"):
232
+ value = value[1:-1]
233
+
234
+ # Handle numbers
235
+ if value.replace('.', '', 1).replace('-', '', 1).isdigit():
236
+ try:
237
+ value = float(value) if '.' in value else int(value)
238
+ except:
239
+ pass
240
+
241
+ data[key] = value
242
+
243
+ return data
244
+
245
+ def save_prompt(self, relative_path: str, prompt_data: Dict) -> bool:
246
+ """
247
+ Save prompt as Markdown file with YAML frontmatter.
248
+
249
+ Args:
250
+ relative_path: Relative path within library (e.g., "Domain Expertise/Medical.md")
251
+ prompt_data: Dictionary with prompt info and content
252
+
253
+ Returns:
254
+ True if successful
255
+ """
256
+ try:
257
+ if not self.library_dir:
258
+ self.log("✗ Library directory not set")
259
+ return False
260
+
261
+ # Construct full path
262
+ filepath = self.library_dir / relative_path
263
+ filepath.parent.mkdir(parents=True, exist_ok=True)
264
+
265
+ # Build frontmatter
266
+ frontmatter = ['---']
267
+
268
+ # Fields to include in frontmatter (in order)
269
+ frontmatter_fields = [
270
+ 'name', 'description', 'domain', 'version', 'task_type',
271
+ 'favorite',
272
+ # QuickMenu
273
+ 'quickmenu_label', 'quickmenu_grid', 'quickmenu_quickmenu',
274
+ # Legacy (kept for backward compatibility)
275
+ 'quick_run',
276
+ 'folder', 'tags',
277
+ 'created', 'modified'
278
+ ]
279
+
280
+ for field in frontmatter_fields:
281
+ if field in prompt_data:
282
+ value = prompt_data[field]
283
+
284
+ # Format based on type
285
+ if isinstance(value, bool):
286
+ frontmatter.append(f'{field}: {str(value).lower()}')
287
+ elif isinstance(value, list):
288
+ # Format arrays
289
+ items = ', '.join([f'"{item}"' for item in value])
290
+ frontmatter.append(f'{field}: [{items}]')
291
+ elif isinstance(value, str):
292
+ frontmatter.append(f'{field}: "{value}"')
293
+ else:
294
+ frontmatter.append(f'{field}: {value}')
295
+
296
+ frontmatter.append('---')
297
+
298
+ # Get content
299
+ content = prompt_data.get('content', '')
300
+
301
+ # Build final file content
302
+ file_content = '\n'.join(frontmatter) + '\n\n' + content.strip()
303
+
304
+ # Write file
305
+ filepath.write_text(file_content, encoding='utf-8')
306
+
307
+ # Update in-memory storage
308
+ prompt_data['_filepath'] = str(filepath)
309
+ prompt_data['_relative_path'] = relative_path
310
+
311
+ # Keep legacy field in sync
312
+ if 'quickmenu_quickmenu' in prompt_data:
313
+ prompt_data['quick_run'] = bool(prompt_data.get('quickmenu_quickmenu', False))
314
+ self.prompts[relative_path] = prompt_data
315
+
316
+ self.log(f"✓ Saved prompt: {prompt_data.get('name', relative_path)}")
317
+ return True
318
+
319
+ except Exception as e:
320
+ self.log(f"✗ Failed to save prompt: {e}")
321
+ return False
322
+
323
+ def get_folder_structure(self) -> Dict:
324
+ """
325
+ Get hierarchical folder structure with prompts.
326
+
327
+ Returns:
328
+ Nested dictionary representing folder tree
329
+ """
330
+ structure = {}
331
+
332
+ for rel_path, prompt_data in self.prompts.items():
333
+ parts = Path(rel_path).parts
334
+
335
+ # Build nested structure
336
+ current = structure
337
+ for i, part in enumerate(parts[:-1]): # Folders only
338
+ if part not in current:
339
+ current[part] = {'_folders': {}, '_prompts': []}
340
+ current = current[part]['_folders']
341
+
342
+ # Add prompt to final folder
343
+ folder_name = parts[-2] if len(parts) > 1 else '_root'
344
+ if folder_name not in current:
345
+ current[folder_name] = {'_folders': {}, '_prompts': []}
346
+
347
+ current[folder_name]['_prompts'].append({
348
+ 'path': rel_path,
349
+ 'name': prompt_data.get('name', Path(rel_path).stem),
350
+ 'favorite': prompt_data.get('favorite', False),
351
+ 'quick_run': prompt_data.get('quick_run', False),
352
+ 'quickmenu_grid': prompt_data.get('quickmenu_grid', False),
353
+ 'quickmenu_quickmenu': prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)),
354
+ 'quickmenu_label': prompt_data.get('quickmenu_label', prompt_data.get('name', Path(rel_path).stem)),
355
+ })
356
+
357
+ return structure
358
+
359
+ def set_primary_prompt(self, relative_path: str) -> bool:
360
+ """Set the primary (main) active prompt"""
361
+ if relative_path not in self.prompts:
362
+ self.log(f"✗ Prompt not found: {relative_path}")
363
+ return False
364
+
365
+ self.active_primary_prompt = self.prompts[relative_path]['content']
366
+ self.active_primary_prompt_path = relative_path
367
+ self.log(f"✓ Set primary prompt: {self.prompts[relative_path].get('name', relative_path)}")
368
+ return True
369
+
370
+ def set_external_primary_prompt(self, file_path: str) -> Tuple[bool, str]:
371
+ """
372
+ Set an external file (not in library) as the primary prompt.
373
+
374
+ Args:
375
+ file_path: Absolute path to the external prompt file
376
+
377
+ Returns:
378
+ Tuple of (success, display_name or error_message)
379
+ """
380
+ path = Path(file_path)
381
+
382
+ if not path.exists():
383
+ self.log(f"✗ File not found: {file_path}")
384
+ return False, "File not found"
385
+
386
+ try:
387
+ content = path.read_text(encoding='utf-8')
388
+ except Exception as e:
389
+ self.log(f"✗ Error reading file: {e}")
390
+ return False, f"Error reading file: {e}"
391
+
392
+ # Use filename (without extension) as display name
393
+ display_name = path.stem
394
+
395
+ # Mark as external with special prefix
396
+ self.active_primary_prompt = content
397
+ self.active_primary_prompt_path = f"[EXTERNAL] {file_path}"
398
+
399
+ self.log(f"✓ Set external primary prompt: {display_name}")
400
+ return True, display_name
401
+
402
+ def attach_prompt(self, relative_path: str) -> bool:
403
+ """Attach a prompt to the active configuration"""
404
+ if relative_path not in self.prompts:
405
+ self.log(f"✗ Prompt not found: {relative_path}")
406
+ return False
407
+
408
+ # Don't attach if already attached
409
+ if relative_path in self.attached_prompt_paths:
410
+ self.log(f"⚠ Already attached: {relative_path}")
411
+ return False
412
+
413
+ prompt_data = self.prompts[relative_path]
414
+ self.attached_prompts.append(prompt_data['content'])
415
+ self.attached_prompt_paths.append(relative_path)
416
+
417
+ self.log(f"✓ Attached: {prompt_data.get('name', relative_path)}")
418
+ return True
419
+
420
+ def detach_prompt(self, relative_path: str) -> bool:
421
+ """Remove an attached prompt"""
422
+ if relative_path not in self.attached_prompt_paths:
423
+ return False
424
+
425
+ idx = self.attached_prompt_paths.index(relative_path)
426
+ self.attached_prompts.pop(idx)
427
+ self.attached_prompt_paths.pop(idx)
428
+
429
+ self.log(f"✓ Detached: {relative_path}")
430
+ return True
431
+
432
+ def clear_attachments(self):
433
+ """Clear all attached prompts"""
434
+ self.attached_prompts = []
435
+ self.attached_prompt_paths = []
436
+ self.log("✓ Cleared all attachments")
437
+
438
+ def toggle_favorite(self, relative_path: str) -> bool:
439
+ """Toggle favorite status for a prompt"""
440
+ if relative_path not in self.prompts:
441
+ return False
442
+
443
+ prompt_data = self.prompts[relative_path]
444
+ prompt_data['favorite'] = not prompt_data.get('favorite', False)
445
+ prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
446
+
447
+ # Save updated prompt
448
+ self.save_prompt(relative_path, prompt_data)
449
+ self._update_favorites_list()
450
+
451
+ return True
452
+
453
+ def toggle_quick_run(self, relative_path: str) -> bool:
454
+ """Toggle QuickMenu (future app menu) status for a prompt (legacy name: quick_run)."""
455
+ if relative_path not in self.prompts:
456
+ return False
457
+
458
+ 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
461
+ prompt_data['quick_run'] = new_value # keep legacy in sync
462
+ prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
463
+
464
+ # Save updated prompt
465
+ self.save_prompt(relative_path, prompt_data)
466
+ self._update_quick_run_list()
467
+ self._update_quickmenu_grid_list()
468
+
469
+ return True
470
+
471
+ def toggle_quickmenu_grid(self, relative_path: str) -> bool:
472
+ """Toggle whether this prompt appears in the Grid right-click QuickMenu."""
473
+ if relative_path not in self.prompts:
474
+ return False
475
+
476
+ prompt_data = self.prompts[relative_path]
477
+ prompt_data['quickmenu_grid'] = not bool(prompt_data.get('quickmenu_grid', False))
478
+ prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
479
+
480
+ self.save_prompt(relative_path, prompt_data)
481
+ self._update_quickmenu_grid_list()
482
+ return True
483
+
484
+ def _update_favorites_list(self):
485
+ """Update cached favorites list"""
486
+ self._favorites = [
487
+ (path, data.get('name', Path(path).stem))
488
+ for path, data in self.prompts.items()
489
+ if data.get('favorite', False)
490
+ ]
491
+
492
+ def _update_quick_run_list(self):
493
+ """Update cached QuickMenu (future app menu) list (legacy name: quick_run)."""
494
+ self._quick_run = []
495
+ for path, data in self.prompts.items():
496
+ is_enabled = bool(data.get('quickmenu_quickmenu', data.get('quick_run', False)))
497
+ if not is_enabled:
498
+ continue
499
+ label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
500
+ self._quick_run.append((path, label))
501
+
502
+ def _update_quickmenu_grid_list(self):
503
+ """Update cached Grid QuickMenu list."""
504
+ self._quickmenu_grid = []
505
+ for path, data in self.prompts.items():
506
+ if not bool(data.get('quickmenu_grid', False)):
507
+ continue
508
+ label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
509
+ self._quickmenu_grid.append((path, label))
510
+
511
+ def get_favorites(self) -> List[Tuple[str, str]]:
512
+ """Get list of favorite prompts (path, name)"""
513
+ return self._favorites
514
+
515
+ def get_quick_run_prompts(self) -> List[Tuple[str, str]]:
516
+ """Get list of QuickMenu (future app menu) prompts (path, label)."""
517
+ return self._quick_run
518
+
519
+ def get_quickmenu_prompts(self) -> List[Tuple[str, str]]:
520
+ """Alias for get_quick_run_prompts(), using the new naming."""
521
+ return self.get_quick_run_prompts()
522
+
523
+ def get_quickmenu_grid_prompts(self) -> List[Tuple[str, str]]:
524
+ """Get list of prompts shown in the Grid right-click QuickMenu (path, label)."""
525
+ return self._quickmenu_grid
526
+
527
+ def create_folder(self, folder_path: str) -> bool:
528
+ """Create a new folder in the library"""
529
+ try:
530
+ if not self.library_dir:
531
+ return False
532
+
533
+ full_path = self.library_dir / folder_path
534
+ full_path.mkdir(parents=True, exist_ok=True)
535
+
536
+ self.log(f"✓ Created folder: {folder_path}")
537
+ return True
538
+
539
+ except Exception as e:
540
+ self.log(f"✗ Failed to create folder: {e}")
541
+ return False
542
+
543
+ def move_prompt(self, old_path: str, new_path: str) -> bool:
544
+ """Move a prompt to a different folder"""
545
+ try:
546
+ if old_path not in self.prompts:
547
+ return False
548
+
549
+ old_file = Path(self.prompts[old_path]['_filepath'])
550
+ new_file = self.library_dir / new_path
551
+
552
+ # Create destination folder
553
+ new_file.parent.mkdir(parents=True, exist_ok=True)
554
+
555
+ # Move file
556
+ shutil.move(str(old_file), str(new_file))
557
+
558
+ # Update in-memory storage
559
+ prompt_data = self.prompts.pop(old_path)
560
+ prompt_data['_filepath'] = str(new_file)
561
+ prompt_data['_relative_path'] = new_path
562
+ self.prompts[new_path] = prompt_data
563
+
564
+ # Update active references if needed
565
+ if self.active_primary_prompt_path == old_path:
566
+ self.active_primary_prompt_path = new_path
567
+
568
+ if old_path in self.attached_prompt_paths:
569
+ idx = self.attached_prompt_paths.index(old_path)
570
+ self.attached_prompt_paths[idx] = new_path
571
+
572
+ self.log(f"✓ Moved: {old_path} → {new_path}")
573
+ return True
574
+
575
+ except Exception as e:
576
+ self.log(f"✗ Failed to move prompt: {e}")
577
+ return False
578
+
579
+ def move_folder(self, old_folder: str, new_folder: str) -> bool:
580
+ """Move a folder (and all contained prompts/subfolders) within the library."""
581
+ try:
582
+ if not self.library_dir:
583
+ return False
584
+
585
+ old_folder = old_folder or ""
586
+ new_folder = new_folder or ""
587
+
588
+ old_dir = self.library_dir / old_folder
589
+ new_dir = self.library_dir / new_folder
590
+
591
+ if not old_dir.exists() or not old_dir.is_dir():
592
+ return False
593
+
594
+ # Prevent moving a folder into itself / a descendant
595
+ old_parts = Path(old_folder).parts
596
+ new_parts = Path(new_folder).parts
597
+ if old_parts and len(new_parts) >= len(old_parts) and new_parts[:len(old_parts)] == old_parts:
598
+ return False
599
+
600
+ new_dir.parent.mkdir(parents=True, exist_ok=True)
601
+ shutil.move(str(old_dir), str(new_dir))
602
+
603
+ old_prefix = f"{old_folder}/" if old_folder else ""
604
+ new_prefix = f"{new_folder}/" if new_folder else ""
605
+
606
+ def rewrite_path(path: Optional[str]) -> Optional[str]:
607
+ if not path:
608
+ return path
609
+ if old_folder and (path == old_folder or path.startswith(old_prefix)):
610
+ return new_folder + path[len(old_folder):]
611
+ if not old_folder and path:
612
+ # moving root is not supported
613
+ return path
614
+ return path
615
+
616
+ # Update active references (paths only). Caller should reload prompts.
617
+ self.active_primary_prompt_path = rewrite_path(self.active_primary_prompt_path)
618
+
619
+ new_attached = []
620
+ for p in self.attached_prompt_paths:
621
+ new_attached.append(rewrite_path(p))
622
+ self.attached_prompt_paths = new_attached
623
+
624
+ self.log(f"✓ Moved folder: {old_folder} → {new_folder}")
625
+ return True
626
+
627
+ except Exception as e:
628
+ self.log(f"✗ Failed to move folder: {e}")
629
+ return False
630
+
631
+ def delete_prompt(self, relative_path: str) -> bool:
632
+ """Delete a prompt"""
633
+ try:
634
+ if relative_path not in self.prompts:
635
+ return False
636
+
637
+ filepath = Path(self.prompts[relative_path]['_filepath'])
638
+ filepath.unlink()
639
+
640
+ # Remove from memory
641
+ del self.prompts[relative_path]
642
+
643
+ # Clear from active if needed
644
+ if self.active_primary_prompt_path == relative_path:
645
+ self.active_primary_prompt = None
646
+ self.active_primary_prompt_path = None
647
+
648
+ if relative_path in self.attached_prompt_paths:
649
+ self.detach_prompt(relative_path)
650
+
651
+ self.log(f"✓ Deleted: {relative_path}")
652
+ return True
653
+
654
+ except Exception as e:
655
+ self.log(f"✗ Failed to delete prompt: {e}")
656
+ return False