supervertaler 1.9.153__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.
- Supervertaler.py +47886 -0
- modules/__init__.py +10 -0
- modules/ai_actions.py +964 -0
- modules/ai_attachment_manager.py +343 -0
- modules/ai_file_viewer_dialog.py +210 -0
- modules/autofingers_engine.py +466 -0
- modules/cafetran_docx_handler.py +379 -0
- modules/config_manager.py +469 -0
- modules/database_manager.py +1878 -0
- modules/database_migrations.py +417 -0
- modules/dejavurtf_handler.py +779 -0
- modules/document_analyzer.py +427 -0
- modules/docx_handler.py +689 -0
- modules/encoding_repair.py +319 -0
- modules/encoding_repair_Qt.py +393 -0
- modules/encoding_repair_ui.py +481 -0
- modules/feature_manager.py +350 -0
- modules/figure_context_manager.py +340 -0
- modules/file_dialog_helper.py +148 -0
- modules/find_replace.py +164 -0
- modules/find_replace_qt.py +457 -0
- modules/glossary_manager.py +433 -0
- modules/image_extractor.py +188 -0
- modules/keyboard_shortcuts_widget.py +571 -0
- modules/llm_clients.py +1211 -0
- modules/llm_leaderboard.py +737 -0
- modules/llm_superbench_ui.py +1401 -0
- modules/local_llm_setup.py +1104 -0
- modules/model_update_dialog.py +381 -0
- modules/model_version_checker.py +373 -0
- modules/mqxliff_handler.py +638 -0
- modules/non_translatables_manager.py +743 -0
- modules/pdf_rescue_Qt.py +1822 -0
- modules/pdf_rescue_tkinter.py +909 -0
- modules/phrase_docx_handler.py +516 -0
- modules/project_home_panel.py +209 -0
- modules/prompt_assistant.py +357 -0
- modules/prompt_library.py +689 -0
- modules/prompt_library_migration.py +447 -0
- modules/quick_access_sidebar.py +282 -0
- modules/ribbon_widget.py +597 -0
- modules/sdlppx_handler.py +874 -0
- modules/setup_wizard.py +353 -0
- modules/shortcut_manager.py +932 -0
- modules/simple_segmenter.py +128 -0
- modules/spellcheck_manager.py +727 -0
- modules/statuses.py +207 -0
- modules/style_guide_manager.py +315 -0
- modules/superbench_ui.py +1319 -0
- modules/superbrowser.py +329 -0
- modules/supercleaner.py +600 -0
- modules/supercleaner_ui.py +444 -0
- modules/superdocs.py +19 -0
- modules/superdocs_viewer_qt.py +382 -0
- modules/superlookup.py +252 -0
- modules/tag_cleaner.py +260 -0
- modules/tag_manager.py +333 -0
- modules/term_extractor.py +270 -0
- modules/termbase_entry_editor.py +842 -0
- modules/termbase_import_export.py +488 -0
- modules/termbase_manager.py +1060 -0
- modules/termview_widget.py +1172 -0
- modules/theme_manager.py +499 -0
- modules/tm_editor_dialog.py +99 -0
- modules/tm_manager_qt.py +1280 -0
- modules/tm_metadata_manager.py +545 -0
- modules/tmx_editor.py +1461 -0
- modules/tmx_editor_qt.py +2784 -0
- modules/tmx_generator.py +284 -0
- modules/tracked_changes.py +900 -0
- modules/trados_docx_handler.py +430 -0
- modules/translation_memory.py +715 -0
- modules/translation_results_panel.py +2134 -0
- modules/translation_services.py +282 -0
- modules/unified_prompt_library.py +659 -0
- modules/unified_prompt_manager_qt.py +3951 -0
- modules/voice_commands.py +920 -0
- modules/voice_dictation.py +477 -0
- modules/voice_dictation_lite.py +249 -0
- supervertaler-1.9.153.dist-info/METADATA +896 -0
- supervertaler-1.9.153.dist-info/RECORD +85 -0
- supervertaler-1.9.153.dist-info/WHEEL +5 -0
- supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
- supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
- supervertaler-1.9.153.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,659 @@
|
|
|
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)
|
|
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
|
+
# Support legacy quickmenu_quickmenu field (rename to sv_quickmenu)
|
|
175
|
+
if 'quickmenu_quickmenu' in prompt_data:
|
|
176
|
+
prompt_data['sv_quickmenu'] = prompt_data['quickmenu_quickmenu']
|
|
177
|
+
prompt_data['sv_quickmenu'] = bool(
|
|
178
|
+
prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False))
|
|
179
|
+
)
|
|
180
|
+
# Keep legacy field in sync so older code/versions still behave.
|
|
181
|
+
prompt_data['quick_run'] = bool(prompt_data['sv_quickmenu'])
|
|
182
|
+
|
|
183
|
+
# New QuickMenu fields
|
|
184
|
+
prompt_data.setdefault('quickmenu_grid', False)
|
|
185
|
+
prompt_data.setdefault('quickmenu_label', prompt_data.get('name', filepath.stem))
|
|
186
|
+
prompt_data.setdefault('tags', [])
|
|
187
|
+
|
|
188
|
+
return prompt_data
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
self.log(f"⚠ Failed to parse {filepath.name}: {e}")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def _parse_yaml(self, yaml_str: str) -> Dict:
|
|
195
|
+
"""
|
|
196
|
+
Simple YAML parser for frontmatter.
|
|
197
|
+
|
|
198
|
+
Supports:
|
|
199
|
+
- Simple strings: key: "value" or key: value
|
|
200
|
+
- Booleans: key: true/false
|
|
201
|
+
- Numbers: key: 1.0
|
|
202
|
+
- Arrays: tags: ["item1", "item2"] or tags: [item1, item2]
|
|
203
|
+
"""
|
|
204
|
+
data = {}
|
|
205
|
+
|
|
206
|
+
for line in yaml_str.strip().split('\n'):
|
|
207
|
+
line = line.strip()
|
|
208
|
+
if not line or line.startswith('#'):
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if ':' not in line:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
key, value = line.split(':', 1)
|
|
215
|
+
key = key.strip()
|
|
216
|
+
value = value.strip()
|
|
217
|
+
|
|
218
|
+
# Handle arrays
|
|
219
|
+
if value.startswith('[') and value.endswith(']'):
|
|
220
|
+
# Remove brackets and split by comma
|
|
221
|
+
array_str = value[1:-1]
|
|
222
|
+
items = [item.strip().strip('"').strip("'") for item in array_str.split(',')]
|
|
223
|
+
data[key] = [item for item in items if item] # Filter empty
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
# Handle booleans
|
|
227
|
+
if value.lower() in ['true', 'false']:
|
|
228
|
+
data[key] = value.lower() == 'true'
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# Remove quotes
|
|
232
|
+
if value.startswith('"') and value.endswith('"'):
|
|
233
|
+
value = value[1:-1]
|
|
234
|
+
elif value.startswith("'") and value.endswith("'"):
|
|
235
|
+
value = value[1:-1]
|
|
236
|
+
|
|
237
|
+
# Handle numbers
|
|
238
|
+
if value.replace('.', '', 1).replace('-', '', 1).isdigit():
|
|
239
|
+
try:
|
|
240
|
+
value = float(value) if '.' in value else int(value)
|
|
241
|
+
except:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
data[key] = value
|
|
245
|
+
|
|
246
|
+
return data
|
|
247
|
+
|
|
248
|
+
def save_prompt(self, relative_path: str, prompt_data: Dict) -> bool:
|
|
249
|
+
"""
|
|
250
|
+
Save prompt as Markdown file with YAML frontmatter.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
relative_path: Relative path within library (e.g., "Domain Expertise/Medical.md")
|
|
254
|
+
prompt_data: Dictionary with prompt info and content
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if successful
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
if not self.library_dir:
|
|
261
|
+
self.log("✗ Library directory not set")
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
# Construct full path
|
|
265
|
+
filepath = self.library_dir / relative_path
|
|
266
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
# Build frontmatter
|
|
269
|
+
frontmatter = ['---']
|
|
270
|
+
|
|
271
|
+
# Fields to include in frontmatter (in order)
|
|
272
|
+
frontmatter_fields = [
|
|
273
|
+
'name', 'description', 'domain', 'version', 'task_type',
|
|
274
|
+
'favorite',
|
|
275
|
+
# QuickMenu
|
|
276
|
+
'quickmenu_label', 'quickmenu_grid', 'sv_quickmenu',
|
|
277
|
+
# Legacy (kept for backward compatibility)
|
|
278
|
+
'quick_run',
|
|
279
|
+
'folder', 'tags',
|
|
280
|
+
'created', 'modified'
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
for field in frontmatter_fields:
|
|
284
|
+
if field in prompt_data:
|
|
285
|
+
value = prompt_data[field]
|
|
286
|
+
|
|
287
|
+
# Format based on type
|
|
288
|
+
if isinstance(value, bool):
|
|
289
|
+
frontmatter.append(f'{field}: {str(value).lower()}')
|
|
290
|
+
elif isinstance(value, list):
|
|
291
|
+
# Format arrays
|
|
292
|
+
items = ', '.join([f'"{item}"' for item in value])
|
|
293
|
+
frontmatter.append(f'{field}: [{items}]')
|
|
294
|
+
elif isinstance(value, str):
|
|
295
|
+
frontmatter.append(f'{field}: "{value}"')
|
|
296
|
+
else:
|
|
297
|
+
frontmatter.append(f'{field}: {value}')
|
|
298
|
+
|
|
299
|
+
frontmatter.append('---')
|
|
300
|
+
|
|
301
|
+
# Get content
|
|
302
|
+
content = prompt_data.get('content', '')
|
|
303
|
+
|
|
304
|
+
# Build final file content
|
|
305
|
+
file_content = '\n'.join(frontmatter) + '\n\n' + content.strip()
|
|
306
|
+
|
|
307
|
+
# Write file
|
|
308
|
+
filepath.write_text(file_content, encoding='utf-8')
|
|
309
|
+
|
|
310
|
+
# Update in-memory storage
|
|
311
|
+
prompt_data['_filepath'] = str(filepath)
|
|
312
|
+
prompt_data['_relative_path'] = relative_path
|
|
313
|
+
|
|
314
|
+
# Keep legacy field in sync
|
|
315
|
+
if 'sv_quickmenu' in prompt_data:
|
|
316
|
+
prompt_data['quick_run'] = bool(prompt_data.get('sv_quickmenu', False))
|
|
317
|
+
self.prompts[relative_path] = prompt_data
|
|
318
|
+
|
|
319
|
+
self.log(f"✓ Saved prompt: {prompt_data.get('name', relative_path)}")
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
self.log(f"✗ Failed to save prompt: {e}")
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
def get_folder_structure(self) -> Dict:
|
|
327
|
+
"""
|
|
328
|
+
Get hierarchical folder structure with prompts.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Nested dictionary representing folder tree
|
|
332
|
+
"""
|
|
333
|
+
structure = {}
|
|
334
|
+
|
|
335
|
+
for rel_path, prompt_data in self.prompts.items():
|
|
336
|
+
parts = Path(rel_path).parts
|
|
337
|
+
|
|
338
|
+
# Build nested structure
|
|
339
|
+
current = structure
|
|
340
|
+
for i, part in enumerate(parts[:-1]): # Folders only
|
|
341
|
+
if part not in current:
|
|
342
|
+
current[part] = {'_folders': {}, '_prompts': []}
|
|
343
|
+
current = current[part]['_folders']
|
|
344
|
+
|
|
345
|
+
# Add prompt to final folder
|
|
346
|
+
folder_name = parts[-2] if len(parts) > 1 else '_root'
|
|
347
|
+
if folder_name not in current:
|
|
348
|
+
current[folder_name] = {'_folders': {}, '_prompts': []}
|
|
349
|
+
|
|
350
|
+
current[folder_name]['_prompts'].append({
|
|
351
|
+
'path': rel_path,
|
|
352
|
+
'name': prompt_data.get('name', Path(rel_path).stem),
|
|
353
|
+
'favorite': prompt_data.get('favorite', False),
|
|
354
|
+
'quick_run': prompt_data.get('quick_run', False),
|
|
355
|
+
'quickmenu_grid': prompt_data.get('quickmenu_grid', False),
|
|
356
|
+
'quickmenu_quickmenu': prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)),
|
|
357
|
+
'quickmenu_label': prompt_data.get('quickmenu_label', prompt_data.get('name', Path(rel_path).stem)),
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
return structure
|
|
361
|
+
|
|
362
|
+
def set_primary_prompt(self, relative_path: str) -> bool:
|
|
363
|
+
"""Set the primary (main) active prompt"""
|
|
364
|
+
if relative_path not in self.prompts:
|
|
365
|
+
self.log(f"✗ Prompt not found: {relative_path}")
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
self.active_primary_prompt = self.prompts[relative_path]['content']
|
|
369
|
+
self.active_primary_prompt_path = relative_path
|
|
370
|
+
self.log(f"✓ Set primary prompt: {self.prompts[relative_path].get('name', relative_path)}")
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
def set_external_primary_prompt(self, file_path: str) -> Tuple[bool, str]:
|
|
374
|
+
"""
|
|
375
|
+
Set an external file (not in library) as the primary prompt.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
file_path: Absolute path to the external prompt file
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Tuple of (success, display_name or error_message)
|
|
382
|
+
"""
|
|
383
|
+
path = Path(file_path)
|
|
384
|
+
|
|
385
|
+
if not path.exists():
|
|
386
|
+
self.log(f"✗ File not found: {file_path}")
|
|
387
|
+
return False, "File not found"
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
content = path.read_text(encoding='utf-8')
|
|
391
|
+
except Exception as e:
|
|
392
|
+
self.log(f"✗ Error reading file: {e}")
|
|
393
|
+
return False, f"Error reading file: {e}"
|
|
394
|
+
|
|
395
|
+
# Use filename (without extension) as display name
|
|
396
|
+
display_name = path.stem
|
|
397
|
+
|
|
398
|
+
# Mark as external with special prefix
|
|
399
|
+
self.active_primary_prompt = content
|
|
400
|
+
self.active_primary_prompt_path = f"[EXTERNAL] {file_path}"
|
|
401
|
+
|
|
402
|
+
self.log(f"✓ Set external primary prompt: {display_name}")
|
|
403
|
+
return True, display_name
|
|
404
|
+
|
|
405
|
+
def attach_prompt(self, relative_path: str) -> bool:
|
|
406
|
+
"""Attach a prompt to the active configuration"""
|
|
407
|
+
if relative_path not in self.prompts:
|
|
408
|
+
self.log(f"✗ Prompt not found: {relative_path}")
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
# Don't attach if already attached
|
|
412
|
+
if relative_path in self.attached_prompt_paths:
|
|
413
|
+
self.log(f"⚠ Already attached: {relative_path}")
|
|
414
|
+
return False
|
|
415
|
+
|
|
416
|
+
prompt_data = self.prompts[relative_path]
|
|
417
|
+
self.attached_prompts.append(prompt_data['content'])
|
|
418
|
+
self.attached_prompt_paths.append(relative_path)
|
|
419
|
+
|
|
420
|
+
self.log(f"✓ Attached: {prompt_data.get('name', relative_path)}")
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
def detach_prompt(self, relative_path: str) -> bool:
|
|
424
|
+
"""Remove an attached prompt"""
|
|
425
|
+
if relative_path not in self.attached_prompt_paths:
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
idx = self.attached_prompt_paths.index(relative_path)
|
|
429
|
+
self.attached_prompts.pop(idx)
|
|
430
|
+
self.attached_prompt_paths.pop(idx)
|
|
431
|
+
|
|
432
|
+
self.log(f"✓ Detached: {relative_path}")
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
def clear_attachments(self):
|
|
436
|
+
"""Clear all attached prompts"""
|
|
437
|
+
self.attached_prompts = []
|
|
438
|
+
self.attached_prompt_paths = []
|
|
439
|
+
self.log("✓ Cleared all attachments")
|
|
440
|
+
|
|
441
|
+
def toggle_favorite(self, relative_path: str) -> bool:
|
|
442
|
+
"""Toggle favorite status for a prompt"""
|
|
443
|
+
if relative_path not in self.prompts:
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
prompt_data = self.prompts[relative_path]
|
|
447
|
+
prompt_data['favorite'] = not prompt_data.get('favorite', False)
|
|
448
|
+
prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
|
|
449
|
+
|
|
450
|
+
# Save updated prompt
|
|
451
|
+
self.save_prompt(relative_path, prompt_data)
|
|
452
|
+
self._update_favorites_list()
|
|
453
|
+
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
def toggle_quick_run(self, relative_path: str) -> bool:
|
|
457
|
+
"""Toggle QuickMenu (future app menu) status for a prompt (legacy name: quick_run)."""
|
|
458
|
+
if relative_path not in self.prompts:
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
prompt_data = self.prompts[relative_path]
|
|
462
|
+
new_value = not bool(prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)))
|
|
463
|
+
prompt_data['sv_quickmenu'] = new_value
|
|
464
|
+
prompt_data['quick_run'] = new_value # keep legacy in sync
|
|
465
|
+
prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
|
|
466
|
+
|
|
467
|
+
# Save updated prompt
|
|
468
|
+
self.save_prompt(relative_path, prompt_data)
|
|
469
|
+
self._update_quick_run_list()
|
|
470
|
+
self._update_quickmenu_grid_list()
|
|
471
|
+
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
def toggle_quickmenu_grid(self, relative_path: str) -> bool:
|
|
475
|
+
"""Toggle whether this prompt appears in the Grid right-click QuickMenu."""
|
|
476
|
+
if relative_path not in self.prompts:
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
prompt_data = self.prompts[relative_path]
|
|
480
|
+
prompt_data['quickmenu_grid'] = not bool(prompt_data.get('quickmenu_grid', False))
|
|
481
|
+
prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
|
|
482
|
+
|
|
483
|
+
self.save_prompt(relative_path, prompt_data)
|
|
484
|
+
self._update_quickmenu_grid_list()
|
|
485
|
+
return True
|
|
486
|
+
|
|
487
|
+
def _update_favorites_list(self):
|
|
488
|
+
"""Update cached favorites list"""
|
|
489
|
+
self._favorites = [
|
|
490
|
+
(path, data.get('name', Path(path).stem))
|
|
491
|
+
for path, data in self.prompts.items()
|
|
492
|
+
if data.get('favorite', False)
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
def _update_quick_run_list(self):
|
|
496
|
+
"""Update cached QuickMenu (future app menu) list (legacy name: quick_run)."""
|
|
497
|
+
self._quick_run = []
|
|
498
|
+
for path, data in self.prompts.items():
|
|
499
|
+
is_enabled = bool(data.get('sv_quickmenu', data.get('quick_run', False)))
|
|
500
|
+
if not is_enabled:
|
|
501
|
+
continue
|
|
502
|
+
label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
|
|
503
|
+
self._quick_run.append((path, label))
|
|
504
|
+
|
|
505
|
+
def _update_quickmenu_grid_list(self):
|
|
506
|
+
"""Update cached Grid QuickMenu list."""
|
|
507
|
+
self._quickmenu_grid = []
|
|
508
|
+
for path, data in self.prompts.items():
|
|
509
|
+
if not bool(data.get('quickmenu_grid', False)):
|
|
510
|
+
continue
|
|
511
|
+
label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
|
|
512
|
+
self._quickmenu_grid.append((path, label))
|
|
513
|
+
|
|
514
|
+
def get_favorites(self) -> List[Tuple[str, str]]:
|
|
515
|
+
"""Get list of favorite prompts (path, name)"""
|
|
516
|
+
return self._favorites
|
|
517
|
+
|
|
518
|
+
def get_quick_run_prompts(self) -> List[Tuple[str, str]]:
|
|
519
|
+
"""Get list of QuickMenu (future app menu) prompts (path, label)."""
|
|
520
|
+
return self._quick_run
|
|
521
|
+
|
|
522
|
+
def get_quickmenu_prompts(self) -> List[Tuple[str, str]]:
|
|
523
|
+
"""Alias for get_quick_run_prompts(), using the new naming."""
|
|
524
|
+
return self.get_quick_run_prompts()
|
|
525
|
+
|
|
526
|
+
def get_quickmenu_grid_prompts(self) -> List[Tuple[str, str]]:
|
|
527
|
+
"""Get list of prompts shown in the Grid right-click QuickMenu (path, label)."""
|
|
528
|
+
return self._quickmenu_grid
|
|
529
|
+
|
|
530
|
+
def create_folder(self, folder_path: str) -> bool:
|
|
531
|
+
"""Create a new folder in the library"""
|
|
532
|
+
try:
|
|
533
|
+
if not self.library_dir:
|
|
534
|
+
return False
|
|
535
|
+
|
|
536
|
+
full_path = self.library_dir / folder_path
|
|
537
|
+
full_path.mkdir(parents=True, exist_ok=True)
|
|
538
|
+
|
|
539
|
+
self.log(f"✓ Created folder: {folder_path}")
|
|
540
|
+
return True
|
|
541
|
+
|
|
542
|
+
except Exception as e:
|
|
543
|
+
self.log(f"✗ Failed to create folder: {e}")
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
def move_prompt(self, old_path: str, new_path: str) -> bool:
|
|
547
|
+
"""Move a prompt to a different folder"""
|
|
548
|
+
try:
|
|
549
|
+
if old_path not in self.prompts:
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
old_file = Path(self.prompts[old_path]['_filepath'])
|
|
553
|
+
new_file = self.library_dir / new_path
|
|
554
|
+
|
|
555
|
+
# Create destination folder
|
|
556
|
+
new_file.parent.mkdir(parents=True, exist_ok=True)
|
|
557
|
+
|
|
558
|
+
# Move file
|
|
559
|
+
shutil.move(str(old_file), str(new_file))
|
|
560
|
+
|
|
561
|
+
# Update in-memory storage
|
|
562
|
+
prompt_data = self.prompts.pop(old_path)
|
|
563
|
+
prompt_data['_filepath'] = str(new_file)
|
|
564
|
+
prompt_data['_relative_path'] = new_path
|
|
565
|
+
self.prompts[new_path] = prompt_data
|
|
566
|
+
|
|
567
|
+
# Update active references if needed
|
|
568
|
+
if self.active_primary_prompt_path == old_path:
|
|
569
|
+
self.active_primary_prompt_path = new_path
|
|
570
|
+
|
|
571
|
+
if old_path in self.attached_prompt_paths:
|
|
572
|
+
idx = self.attached_prompt_paths.index(old_path)
|
|
573
|
+
self.attached_prompt_paths[idx] = new_path
|
|
574
|
+
|
|
575
|
+
self.log(f"✓ Moved: {old_path} → {new_path}")
|
|
576
|
+
return True
|
|
577
|
+
|
|
578
|
+
except Exception as e:
|
|
579
|
+
self.log(f"✗ Failed to move prompt: {e}")
|
|
580
|
+
return False
|
|
581
|
+
|
|
582
|
+
def move_folder(self, old_folder: str, new_folder: str) -> bool:
|
|
583
|
+
"""Move a folder (and all contained prompts/subfolders) within the library."""
|
|
584
|
+
try:
|
|
585
|
+
if not self.library_dir:
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
old_folder = old_folder or ""
|
|
589
|
+
new_folder = new_folder or ""
|
|
590
|
+
|
|
591
|
+
old_dir = self.library_dir / old_folder
|
|
592
|
+
new_dir = self.library_dir / new_folder
|
|
593
|
+
|
|
594
|
+
if not old_dir.exists() or not old_dir.is_dir():
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
# Prevent moving a folder into itself / a descendant
|
|
598
|
+
old_parts = Path(old_folder).parts
|
|
599
|
+
new_parts = Path(new_folder).parts
|
|
600
|
+
if old_parts and len(new_parts) >= len(old_parts) and new_parts[:len(old_parts)] == old_parts:
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
new_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
604
|
+
shutil.move(str(old_dir), str(new_dir))
|
|
605
|
+
|
|
606
|
+
old_prefix = f"{old_folder}/" if old_folder else ""
|
|
607
|
+
new_prefix = f"{new_folder}/" if new_folder else ""
|
|
608
|
+
|
|
609
|
+
def rewrite_path(path: Optional[str]) -> Optional[str]:
|
|
610
|
+
if not path:
|
|
611
|
+
return path
|
|
612
|
+
if old_folder and (path == old_folder or path.startswith(old_prefix)):
|
|
613
|
+
return new_folder + path[len(old_folder):]
|
|
614
|
+
if not old_folder and path:
|
|
615
|
+
# moving root is not supported
|
|
616
|
+
return path
|
|
617
|
+
return path
|
|
618
|
+
|
|
619
|
+
# Update active references (paths only). Caller should reload prompts.
|
|
620
|
+
self.active_primary_prompt_path = rewrite_path(self.active_primary_prompt_path)
|
|
621
|
+
|
|
622
|
+
new_attached = []
|
|
623
|
+
for p in self.attached_prompt_paths:
|
|
624
|
+
new_attached.append(rewrite_path(p))
|
|
625
|
+
self.attached_prompt_paths = new_attached
|
|
626
|
+
|
|
627
|
+
self.log(f"✓ Moved folder: {old_folder} → {new_folder}")
|
|
628
|
+
return True
|
|
629
|
+
|
|
630
|
+
except Exception as e:
|
|
631
|
+
self.log(f"✗ Failed to move folder: {e}")
|
|
632
|
+
return False
|
|
633
|
+
|
|
634
|
+
def delete_prompt(self, relative_path: str) -> bool:
|
|
635
|
+
"""Delete a prompt"""
|
|
636
|
+
try:
|
|
637
|
+
if relative_path not in self.prompts:
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
filepath = Path(self.prompts[relative_path]['_filepath'])
|
|
641
|
+
filepath.unlink()
|
|
642
|
+
|
|
643
|
+
# Remove from memory
|
|
644
|
+
del self.prompts[relative_path]
|
|
645
|
+
|
|
646
|
+
# Clear from active if needed
|
|
647
|
+
if self.active_primary_prompt_path == relative_path:
|
|
648
|
+
self.active_primary_prompt = None
|
|
649
|
+
self.active_primary_prompt_path = None
|
|
650
|
+
|
|
651
|
+
if relative_path in self.attached_prompt_paths:
|
|
652
|
+
self.detach_prompt(relative_path)
|
|
653
|
+
|
|
654
|
+
self.log(f"✓ Deleted: {relative_path}")
|
|
655
|
+
return True
|
|
656
|
+
|
|
657
|
+
except Exception as e:
|
|
658
|
+
self.log(f"✗ Failed to delete prompt: {e}")
|
|
659
|
+
return False
|