pomera-ai-commander 1.2.7 → 1.2.9
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.
- package/bin/pomera-create-shortcut.js +51 -0
- package/bin/pomera.js +68 -0
- package/core/database_schema.py +24 -1
- package/core/database_schema_manager.py +4 -2
- package/core/database_settings_manager.py +25 -2
- package/core/dialog_manager.py +4 -4
- package/core/efficient_line_numbers.py +5 -4
- package/core/load_presets_dialog.py +460 -0
- package/core/mcp/tool_registry.py +327 -0
- package/core/settings_defaults_registry.py +159 -15
- package/core/tool_search_widget.py +85 -5
- package/create_shortcut.py +12 -4
- package/mcp.json +1 -1
- package/package.json +4 -2
- package/pomera.py +760 -25
- package/tools/base64_tools.py +4 -4
- package/tools/case_tool.py +4 -4
- package/tools/curl_settings.py +12 -1
- package/tools/curl_tool.py +176 -11
- package/tools/notes_widget.py +8 -1
- package/tools/tool_loader.py +20 -9
- package/tools/url_content_reader.py +402 -0
- package/tools/web_search.py +522 -0
package/pomera.py
CHANGED
|
@@ -510,6 +510,81 @@ except ImportError as e:
|
|
|
510
510
|
TOOL_SEARCH_WIDGET_AVAILABLE = False
|
|
511
511
|
print(f"Tool Search Widget not available: {e}")
|
|
512
512
|
|
|
513
|
+
|
|
514
|
+
class StartupProfiler:
|
|
515
|
+
"""
|
|
516
|
+
Startup profiling utility to diagnose slow initialization.
|
|
517
|
+
|
|
518
|
+
Usage:
|
|
519
|
+
profiler = StartupProfiler()
|
|
520
|
+
profiler.start("Stage Name")
|
|
521
|
+
# ... do work ...
|
|
522
|
+
profiler.end("Stage Name")
|
|
523
|
+
profiler.summary() # Prints timing report to console
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
def __init__(self, enabled: bool = True):
|
|
527
|
+
self.enabled = enabled
|
|
528
|
+
self.stages: Dict[str, Dict[str, float]] = {}
|
|
529
|
+
self.order: List[str] = []
|
|
530
|
+
self._total_start = time.perf_counter()
|
|
531
|
+
|
|
532
|
+
def start(self, stage_name: str) -> None:
|
|
533
|
+
"""Start timing a stage."""
|
|
534
|
+
if not self.enabled:
|
|
535
|
+
return
|
|
536
|
+
self.stages[stage_name] = {"start": time.perf_counter(), "end": None}
|
|
537
|
+
if stage_name not in self.order:
|
|
538
|
+
self.order.append(stage_name)
|
|
539
|
+
|
|
540
|
+
def end(self, stage_name: str) -> None:
|
|
541
|
+
"""End timing a stage."""
|
|
542
|
+
if not self.enabled or stage_name not in self.stages:
|
|
543
|
+
return
|
|
544
|
+
self.stages[stage_name]["end"] = time.perf_counter()
|
|
545
|
+
|
|
546
|
+
def duration(self, stage_name: str) -> float:
|
|
547
|
+
"""Get duration of a completed stage in milliseconds."""
|
|
548
|
+
if stage_name not in self.stages:
|
|
549
|
+
return 0.0
|
|
550
|
+
stage = self.stages[stage_name]
|
|
551
|
+
if stage["end"] is None:
|
|
552
|
+
return 0.0
|
|
553
|
+
return (stage["end"] - stage["start"]) * 1000
|
|
554
|
+
|
|
555
|
+
def summary(self) -> str:
|
|
556
|
+
"""Print formatted timing summary to console and return as string."""
|
|
557
|
+
if not self.enabled:
|
|
558
|
+
return ""
|
|
559
|
+
|
|
560
|
+
total_elapsed = (time.perf_counter() - self._total_start) * 1000
|
|
561
|
+
|
|
562
|
+
lines = [
|
|
563
|
+
"\n" + "=" * 60,
|
|
564
|
+
"STARTUP PROFILING REPORT",
|
|
565
|
+
"=" * 60,
|
|
566
|
+
]
|
|
567
|
+
|
|
568
|
+
for stage_name in self.order:
|
|
569
|
+
duration_ms = self.duration(stage_name)
|
|
570
|
+
bar_length = min(int(duration_ms / 50), 30) # 50ms per char, max 30
|
|
571
|
+
bar = "█" * bar_length
|
|
572
|
+
status = "SLOW" if duration_ms > 500 else ""
|
|
573
|
+
lines.append(f" {stage_name:40} {duration_ms:7.1f}ms {bar} {status}")
|
|
574
|
+
|
|
575
|
+
lines.append("-" * 60)
|
|
576
|
+
lines.append(f" {'TOTAL':40} {total_elapsed:7.1f}ms")
|
|
577
|
+
lines.append("=" * 60 + "\n")
|
|
578
|
+
|
|
579
|
+
report = "\n".join(lines)
|
|
580
|
+
print(report)
|
|
581
|
+
return report
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# Global startup profiler instance (enabled by default for diagnostics)
|
|
585
|
+
_startup_profiler = StartupProfiler(enabled=True)
|
|
586
|
+
|
|
587
|
+
|
|
513
588
|
class AppConfig:
|
|
514
589
|
"""Configuration constants for the application."""
|
|
515
590
|
DEFAULT_WINDOW_SIZE = "1200x900"
|
|
@@ -626,14 +701,16 @@ class PromeraAISettingsManager:
|
|
|
626
701
|
|
|
627
702
|
def get_pattern_library(self) -> List[Dict[str, str]]:
|
|
628
703
|
"""Get the regex pattern library."""
|
|
629
|
-
# Initialize pattern library if it doesn't exist
|
|
630
|
-
|
|
704
|
+
# Initialize pattern library if it doesn't exist OR is empty
|
|
705
|
+
existing = self.app.settings.get("pattern_library", None)
|
|
706
|
+
if not existing or len(existing) == 0:
|
|
631
707
|
# Try to import and use the comprehensive pattern library
|
|
632
708
|
try:
|
|
633
709
|
from core.regex_pattern_library import RegexPatternLibrary
|
|
634
710
|
library = RegexPatternLibrary()
|
|
635
711
|
self.app.settings["pattern_library"] = library._convert_to_settings_format()
|
|
636
|
-
|
|
712
|
+
pattern_count = len(self.app.settings.get("pattern_library", []))
|
|
713
|
+
self.app.logger.info(f"Loaded comprehensive pattern library with {pattern_count} patterns")
|
|
637
714
|
except ImportError:
|
|
638
715
|
# Fallback to basic patterns if comprehensive library is not available
|
|
639
716
|
self.app.settings["pattern_library"] = [
|
|
@@ -787,6 +864,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
787
864
|
global DATABASE_SETTINGS_AVAILABLE
|
|
788
865
|
super().__init__()
|
|
789
866
|
|
|
867
|
+
# Start profiling from here
|
|
868
|
+
_startup_profiler.start("Database/Settings Init")
|
|
869
|
+
|
|
790
870
|
# Record startup time for performance monitoring
|
|
791
871
|
self.startup_time = time.time()
|
|
792
872
|
|
|
@@ -819,7 +899,8 @@ class PromeraAIApp(tk.Tk):
|
|
|
819
899
|
"Base64 Encoder/Decoder", "Translator Tools", "Diff Viewer",
|
|
820
900
|
"Line Tools", "Whitespace Tools", "Text Statistics", "Markdown Tools",
|
|
821
901
|
"String Escape Tool", "Number Base Converter", "Text Wrapper",
|
|
822
|
-
"Column Tools", "Timestamp Converter"
|
|
902
|
+
"Column Tools", "Timestamp Converter",
|
|
903
|
+
"Web Search", "URL Reader" # Manual search/fetch only on button click
|
|
823
904
|
]
|
|
824
905
|
|
|
825
906
|
# CORRECTED ORDER: Load settings BEFORE setting up logging
|
|
@@ -864,9 +945,14 @@ class PromeraAIApp(tk.Tk):
|
|
|
864
945
|
self.settings = self.load_settings()
|
|
865
946
|
else:
|
|
866
947
|
self.settings = self.load_settings()
|
|
948
|
+
_startup_profiler.end("Database/Settings Init")
|
|
949
|
+
|
|
950
|
+
_startup_profiler.start("Logging/Audio Setup")
|
|
867
951
|
self.setup_logging()
|
|
868
952
|
self.setup_audio()
|
|
953
|
+
_startup_profiler.end("Logging/Audio Setup")
|
|
869
954
|
|
|
955
|
+
_startup_profiler.start("DialogManager Init")
|
|
870
956
|
# Initialize DialogManager BEFORE optimized components
|
|
871
957
|
if DIALOG_MANAGER_AVAILABLE:
|
|
872
958
|
self.dialog_settings_adapter = DialogSettingsAdapter(self)
|
|
@@ -925,10 +1011,19 @@ class PromeraAIApp(tk.Tk):
|
|
|
925
1011
|
self.logger.warning("Tool Loader not available")
|
|
926
1012
|
|
|
927
1013
|
# Setup optimized components AFTER DialogManager is available
|
|
1014
|
+
_startup_profiler.end("DialogManager Init")
|
|
1015
|
+
|
|
1016
|
+
_startup_profiler.start("Optimized Components")
|
|
928
1017
|
self.setup_optimized_components()
|
|
1018
|
+
_startup_profiler.end("Optimized Components")
|
|
929
1019
|
|
|
1020
|
+
_startup_profiler.start("Create Widgets")
|
|
930
1021
|
self.create_widgets()
|
|
1022
|
+
_startup_profiler.end("Create Widgets")
|
|
1023
|
+
|
|
1024
|
+
_startup_profiler.start("Load Last State")
|
|
931
1025
|
self.load_last_state()
|
|
1026
|
+
_startup_profiler.end("Load Last State")
|
|
932
1027
|
|
|
933
1028
|
# Initialization complete - allow automatic processing
|
|
934
1029
|
self._initializing = False
|
|
@@ -987,6 +1082,11 @@ class PromeraAIApp(tk.Tk):
|
|
|
987
1082
|
self.bind_all("<Control-Shift-h>", self.toggle_options_panel)
|
|
988
1083
|
self.bind_all("<Control-Shift-H>", self.toggle_options_panel) # Windows needs uppercase
|
|
989
1084
|
|
|
1085
|
+
# Set up Load Presets shortcut (Ctrl+Shift+P) - Reset tool settings to defaults
|
|
1086
|
+
if SETTINGS_DEFAULTS_REGISTRY_AVAILABLE:
|
|
1087
|
+
self.bind_all("<Control-Shift-p>", lambda e: self.show_load_presets_dialog())
|
|
1088
|
+
self.bind_all("<Control-Shift-P>", lambda e: self.show_load_presets_dialog())
|
|
1089
|
+
|
|
990
1090
|
# Set up window focus and minimize event handlers for visibility-aware updates
|
|
991
1091
|
if hasattr(self, 'statistics_update_manager') and self.statistics_update_manager:
|
|
992
1092
|
self.bind("<FocusIn>", self._on_window_focus_in)
|
|
@@ -1006,6 +1106,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
1006
1106
|
|
|
1007
1107
|
# Schedule background maintenance tasks
|
|
1008
1108
|
self._schedule_maintenance_tasks()
|
|
1109
|
+
|
|
1110
|
+
# Print startup profiling report
|
|
1111
|
+
_startup_profiler.summary()
|
|
1009
1112
|
|
|
1010
1113
|
def _schedule_maintenance_tasks(self):
|
|
1011
1114
|
"""Schedule periodic background maintenance tasks using Task Scheduler."""
|
|
@@ -1050,6 +1153,50 @@ class PromeraAIApp(tk.Tk):
|
|
|
1050
1153
|
|
|
1051
1154
|
self.logger.info("Background maintenance tasks scheduled (including auto-backup)")
|
|
1052
1155
|
|
|
1156
|
+
def show_load_presets_dialog(self):
|
|
1157
|
+
"""
|
|
1158
|
+
Open the Load Presets dialog to reset tool settings to defaults.
|
|
1159
|
+
|
|
1160
|
+
Accessible via Ctrl+Shift+P keyboard shortcut.
|
|
1161
|
+
"""
|
|
1162
|
+
try:
|
|
1163
|
+
from core.load_presets_dialog import LoadPresetsDialog
|
|
1164
|
+
|
|
1165
|
+
# Get settings manager - prefer database settings manager
|
|
1166
|
+
settings_manager = getattr(self, 'db_settings_manager', None)
|
|
1167
|
+
if not settings_manager:
|
|
1168
|
+
# Fallback to PromeraAISettingsManager
|
|
1169
|
+
settings_manager = PromeraAISettingsManager(self)
|
|
1170
|
+
|
|
1171
|
+
dialog = LoadPresetsDialog(
|
|
1172
|
+
self,
|
|
1173
|
+
settings_manager,
|
|
1174
|
+
logger=self.logger,
|
|
1175
|
+
dialog_manager=self.dialog_manager
|
|
1176
|
+
)
|
|
1177
|
+
dialog.show()
|
|
1178
|
+
self.logger.info("Load Presets dialog opened")
|
|
1179
|
+
|
|
1180
|
+
except ImportError as e:
|
|
1181
|
+
self.logger.error(f"Could not load Load Presets dialog: {e}")
|
|
1182
|
+
if self.dialog_manager:
|
|
1183
|
+
self.dialog_manager.show_error(
|
|
1184
|
+
"Module Not Available",
|
|
1185
|
+
"Load Presets dialog is not available.\n"
|
|
1186
|
+
"Please ensure core/load_presets_dialog.py exists."
|
|
1187
|
+
)
|
|
1188
|
+
else:
|
|
1189
|
+
messagebox.showerror(
|
|
1190
|
+
"Module Not Available",
|
|
1191
|
+
"Load Presets dialog is not available."
|
|
1192
|
+
)
|
|
1193
|
+
except Exception as e:
|
|
1194
|
+
self.logger.error(f"Error opening Load Presets dialog: {e}")
|
|
1195
|
+
if self.dialog_manager:
|
|
1196
|
+
self.dialog_manager.show_error("Error", f"Failed to open dialog: {e}")
|
|
1197
|
+
else:
|
|
1198
|
+
messagebox.showerror("Error", f"Failed to open dialog: {e}")
|
|
1199
|
+
|
|
1053
1200
|
def _show_data_location_dialog(self):
|
|
1054
1201
|
"""Show the Data Location settings dialog."""
|
|
1055
1202
|
from tkinter import filedialog
|
|
@@ -1867,6 +2014,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
1867
2014
|
self.settings["active_input_tab"] = self.input_notebook.index(self.input_notebook.select())
|
|
1868
2015
|
self.settings["active_output_tab"] = self.output_notebook.index(self.output_notebook.select())
|
|
1869
2016
|
|
|
2017
|
+
# Save current tool selection (including Diff Viewer)
|
|
2018
|
+
self.settings["selected_tool"] = self.tool_var.get()
|
|
2019
|
+
|
|
1870
2020
|
# Debug logging to track what's being saved
|
|
1871
2021
|
non_empty_inputs = sum(1 for content in input_contents if content)
|
|
1872
2022
|
non_empty_outputs = sum(1 for content in output_contents if content)
|
|
@@ -1876,9 +2026,19 @@ class PromeraAIApp(tk.Tk):
|
|
|
1876
2026
|
if hasattr(self, 'db_settings_manager') and self.db_settings_manager:
|
|
1877
2027
|
try:
|
|
1878
2028
|
# Database manager handles saving automatically through the proxy
|
|
1879
|
-
#
|
|
1880
|
-
|
|
1881
|
-
|
|
2029
|
+
# Only backup to disk periodically (every 5 seconds max) to avoid performance issues
|
|
2030
|
+
import time
|
|
2031
|
+
current_time = time.time()
|
|
2032
|
+
if not hasattr(self, '_last_disk_backup_time'):
|
|
2033
|
+
self._last_disk_backup_time = 0
|
|
2034
|
+
|
|
2035
|
+
# Only force disk backup every 5 seconds to prevent freezing during typing
|
|
2036
|
+
if current_time - self._last_disk_backup_time > 5.0:
|
|
2037
|
+
self.db_settings_manager.connection_manager.backup_to_disk()
|
|
2038
|
+
self._last_disk_backup_time = current_time
|
|
2039
|
+
self.logger.info("Settings saved to database")
|
|
2040
|
+
else:
|
|
2041
|
+
self.logger.debug("Settings updated (disk backup deferred)")
|
|
1882
2042
|
except Exception as e:
|
|
1883
2043
|
self._handle_error(e, "Saving to database", "Settings",
|
|
1884
2044
|
user_message="Failed to save settings to database, trying JSON fallback",
|
|
@@ -3080,6 +3240,10 @@ class PromeraAIApp(tk.Tk):
|
|
|
3080
3240
|
|
|
3081
3241
|
file_menu.add_separator()
|
|
3082
3242
|
|
|
3243
|
+
# Load Presets (reset tool settings to defaults)
|
|
3244
|
+
if SETTINGS_DEFAULTS_REGISTRY_AVAILABLE:
|
|
3245
|
+
file_menu.add_command(label="Load Presets...", command=self.show_load_presets_dialog, accelerator="Ctrl+Shift+P")
|
|
3246
|
+
|
|
3083
3247
|
# Settings Backup and Recovery submenu
|
|
3084
3248
|
if DATABASE_SETTINGS_AVAILABLE and hasattr(self, 'db_settings_manager'):
|
|
3085
3249
|
backup_menu = tk.Menu(file_menu, tearoff=0)
|
|
@@ -3806,7 +3970,32 @@ class PromeraAIApp(tk.Tk):
|
|
|
3806
3970
|
else:
|
|
3807
3971
|
self.widget_cache = None
|
|
3808
3972
|
|
|
3973
|
+
# Defer tool settings UI creation until after window is visible
|
|
3974
|
+
# This makes startup feel faster by moving ~300ms of UI creation to after mainloop starts
|
|
3975
|
+
self._tool_ui_initialized = False
|
|
3976
|
+
self._show_tool_loading_placeholder()
|
|
3977
|
+
self.after_idle(self._deferred_tool_settings_init)
|
|
3978
|
+
|
|
3979
|
+
def _show_tool_loading_placeholder(self):
|
|
3980
|
+
"""Show lightweight placeholder while tool UI is loading."""
|
|
3981
|
+
self._loading_label = ttk.Label(
|
|
3982
|
+
self.tool_settings_frame,
|
|
3983
|
+
text="Loading tool options...",
|
|
3984
|
+
font=("TkDefaultFont", 10, "italic"),
|
|
3985
|
+
foreground="gray"
|
|
3986
|
+
)
|
|
3987
|
+
self._loading_label.pack(pady=20)
|
|
3988
|
+
|
|
3989
|
+
def _deferred_tool_settings_init(self):
|
|
3990
|
+
"""Initialize tool settings UI after window is visible (faster perceived startup)."""
|
|
3991
|
+
# Remove placeholder
|
|
3992
|
+
if hasattr(self, '_loading_label') and self._loading_label.winfo_exists():
|
|
3993
|
+
self._loading_label.destroy()
|
|
3994
|
+
|
|
3995
|
+
# Now create the actual tool settings UI
|
|
3996
|
+
self._tool_ui_initialized = True
|
|
3809
3997
|
self.update_tool_settings_ui()
|
|
3998
|
+
self.logger.info("Deferred tool settings UI initialization complete")
|
|
3810
3999
|
|
|
3811
4000
|
def _create_tool_search_palette(self, parent):
|
|
3812
4001
|
"""Create the new ToolSearchPalette for tool selection."""
|
|
@@ -4487,17 +4676,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
4487
4676
|
ttk.Checkbutton(memory_frame, text="Enable advanced memory management",
|
|
4488
4677
|
variable=self.memory_enabled_var).pack(anchor=tk.W)
|
|
4489
4678
|
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
("Memory leak detection", "leak_detection", True)
|
|
4494
|
-
]
|
|
4495
|
-
|
|
4496
|
-
self.memory_option_vars = {}
|
|
4497
|
-
for text, key, default in memory_options:
|
|
4498
|
-
var = tk.BooleanVar(value=memory_settings.get(key, default))
|
|
4499
|
-
self.memory_option_vars[key] = var
|
|
4500
|
-
ttk.Checkbutton(memory_frame, text=text, variable=var).pack(anchor=tk.W, padx=(20, 0))
|
|
4679
|
+
# Note: GC optimization, memory pool, and leak detection options were removed
|
|
4680
|
+
# as they are not currently implemented. Memory threshold is functional.
|
|
4681
|
+
self.memory_option_vars = {} # Keep for backwards compatibility
|
|
4501
4682
|
|
|
4502
4683
|
# Memory threshold
|
|
4503
4684
|
mem_threshold_frame = ttk.Frame(memory_frame)
|
|
@@ -4590,9 +4771,6 @@ class PromeraAIApp(tk.Tk):
|
|
|
4590
4771
|
},
|
|
4591
4772
|
"memory_management": {
|
|
4592
4773
|
"enabled": self.memory_enabled_var.get(),
|
|
4593
|
-
"gc_optimization": self.memory_option_vars["gc_optimization"].get(),
|
|
4594
|
-
"memory_pool": self.memory_option_vars["memory_pool"].get(),
|
|
4595
|
-
"leak_detection": self.memory_option_vars["leak_detection"].get(),
|
|
4596
4774
|
"memory_threshold_mb": int(self.memory_threshold_var.get())
|
|
4597
4775
|
},
|
|
4598
4776
|
"ui_optimizations": {
|
|
@@ -5197,8 +5375,10 @@ class PromeraAIApp(tk.Tk):
|
|
|
5197
5375
|
self.create_column_tools_widget(parent_frame)
|
|
5198
5376
|
elif tool_name == "Timestamp Converter":
|
|
5199
5377
|
self.create_timestamp_converter_widget(parent_frame)
|
|
5200
|
-
|
|
5201
|
-
|
|
5378
|
+
elif tool_name == "Web Search":
|
|
5379
|
+
self.create_web_search_options(parent_frame)
|
|
5380
|
+
elif tool_name == "URL Reader":
|
|
5381
|
+
self.create_url_reader_options(parent_frame)
|
|
5202
5382
|
|
|
5203
5383
|
|
|
5204
5384
|
|
|
@@ -5318,6 +5498,510 @@ class PromeraAIApp(tk.Tk):
|
|
|
5318
5498
|
except:
|
|
5319
5499
|
pass # Continue if font application fails
|
|
5320
5500
|
|
|
5501
|
+
def create_web_search_options(self, parent):
|
|
5502
|
+
"""Creates the Web Search tool options panel with tabbed interface like AI Tools."""
|
|
5503
|
+
# Engine configuration: (display_name, engine_key, api_key_url, needs_api_key, needs_cse_id)
|
|
5504
|
+
self.web_search_engines = [
|
|
5505
|
+
("DuckDuckGo", "duckduckgo", None, False, False), # Free, no API key
|
|
5506
|
+
("Tavily", "tavily", "https://tavily.com/", True, False),
|
|
5507
|
+
("Google", "google", "https://programmablesearchengine.google.com/", True, True), # Needs CSE ID
|
|
5508
|
+
("Brave", "brave", "https://brave.com/search/api/", True, False),
|
|
5509
|
+
("SerpApi", "serpapi", "https://serpapi.com/", True, False),
|
|
5510
|
+
("Serper", "serper", "https://serper.dev/", True, False),
|
|
5511
|
+
]
|
|
5512
|
+
|
|
5513
|
+
# Create notebook for tabs
|
|
5514
|
+
self.web_search_notebook = ttk.Notebook(parent)
|
|
5515
|
+
self.web_search_notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
5516
|
+
|
|
5517
|
+
# Track API key and settings vars per engine
|
|
5518
|
+
self.web_search_api_vars = {}
|
|
5519
|
+
self.web_search_cse_vars = {} # For Google CSE ID
|
|
5520
|
+
self.web_search_count_vars = {}
|
|
5521
|
+
|
|
5522
|
+
for display_name, engine_key, api_url, needs_api_key, needs_cse_id in self.web_search_engines:
|
|
5523
|
+
tab = ttk.Frame(self.web_search_notebook)
|
|
5524
|
+
self.web_search_notebook.add(tab, text=display_name)
|
|
5525
|
+
|
|
5526
|
+
# API Configuration row
|
|
5527
|
+
api_frame = ttk.LabelFrame(tab, text="API Configuration", padding=5)
|
|
5528
|
+
api_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
5529
|
+
|
|
5530
|
+
if needs_api_key:
|
|
5531
|
+
# API Key field
|
|
5532
|
+
ttk.Label(api_frame, text="API Key:").pack(side=tk.LEFT, padx=(5, 5))
|
|
5533
|
+
api_var = tk.StringVar()
|
|
5534
|
+
api_entry = ttk.Entry(api_frame, textvariable=api_var, width=30, show="*")
|
|
5535
|
+
api_entry.pack(side=tk.LEFT, padx=5)
|
|
5536
|
+
|
|
5537
|
+
# Load existing key
|
|
5538
|
+
saved_key = self._load_web_search_api_key(engine_key)
|
|
5539
|
+
if saved_key:
|
|
5540
|
+
api_var.set(saved_key)
|
|
5541
|
+
|
|
5542
|
+
# Auto-save on focus out
|
|
5543
|
+
api_entry.bind("<FocusOut>", lambda e, k=engine_key, v=api_var: self._save_web_search_api_key(k, v.get()))
|
|
5544
|
+
self.web_search_api_vars[engine_key] = api_var
|
|
5545
|
+
|
|
5546
|
+
# Get API Key button
|
|
5547
|
+
get_key_btn = ttk.Button(
|
|
5548
|
+
api_frame,
|
|
5549
|
+
text="Get API Key",
|
|
5550
|
+
command=lambda url=api_url: webbrowser.open_new(url)
|
|
5551
|
+
)
|
|
5552
|
+
get_key_btn.pack(side=tk.LEFT, padx=5)
|
|
5553
|
+
|
|
5554
|
+
# Google needs CSE ID
|
|
5555
|
+
if needs_cse_id:
|
|
5556
|
+
ttk.Label(api_frame, text="CSE ID:").pack(side=tk.LEFT, padx=(15, 5))
|
|
5557
|
+
cse_var = tk.StringVar()
|
|
5558
|
+
cse_entry = ttk.Entry(api_frame, textvariable=cse_var, width=20)
|
|
5559
|
+
cse_entry.pack(side=tk.LEFT, padx=5)
|
|
5560
|
+
|
|
5561
|
+
# Load saved CSE ID
|
|
5562
|
+
saved_cse = self._get_web_search_setting(engine_key, "cse_id", "")
|
|
5563
|
+
if saved_cse:
|
|
5564
|
+
cse_var.set(saved_cse)
|
|
5565
|
+
|
|
5566
|
+
cse_entry.bind("<FocusOut>", lambda e, k=engine_key, v=cse_var: self._save_web_search_setting(k, "cse_id", v.get()))
|
|
5567
|
+
self.web_search_cse_vars[engine_key] = cse_var
|
|
5568
|
+
|
|
5569
|
+
# Get CSE ID button
|
|
5570
|
+
get_cse_btn = ttk.Button(
|
|
5571
|
+
api_frame,
|
|
5572
|
+
text="Get CSE ID",
|
|
5573
|
+
command=lambda: webbrowser.open_new("https://programmablesearchengine.google.com/controlpanel/all")
|
|
5574
|
+
)
|
|
5575
|
+
get_cse_btn.pack(side=tk.LEFT, padx=5)
|
|
5576
|
+
else:
|
|
5577
|
+
ttk.Label(api_frame, text="No API key required (free)", foreground="green").pack(side=tk.LEFT, padx=10)
|
|
5578
|
+
|
|
5579
|
+
# Search Configuration row
|
|
5580
|
+
config_frame = ttk.LabelFrame(tab, text="Search Configuration", padding=5)
|
|
5581
|
+
config_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
5582
|
+
|
|
5583
|
+
# Result count
|
|
5584
|
+
ttk.Label(config_frame, text="Results:").pack(side=tk.LEFT, padx=(5, 5))
|
|
5585
|
+
saved_count = self._get_web_search_setting(engine_key, "count", 5)
|
|
5586
|
+
count_var = tk.StringVar(value=str(saved_count))
|
|
5587
|
+
count_spinbox = ttk.Spinbox(config_frame, from_=1, to=20, width=5, textvariable=count_var)
|
|
5588
|
+
count_spinbox.pack(side=tk.LEFT, padx=5)
|
|
5589
|
+
count_spinbox.bind("<FocusOut>", lambda e, k=engine_key, v=count_var: self._save_web_search_setting(k, "count", int(v.get())))
|
|
5590
|
+
self.web_search_count_vars[engine_key] = count_var
|
|
5591
|
+
|
|
5592
|
+
# Search button
|
|
5593
|
+
search_btn = ttk.Button(
|
|
5594
|
+
config_frame,
|
|
5595
|
+
text="Search",
|
|
5596
|
+
command=lambda k=engine_key: self._do_web_search(k)
|
|
5597
|
+
)
|
|
5598
|
+
search_btn.pack(side=tk.LEFT, padx=20)
|
|
5599
|
+
|
|
5600
|
+
# Bind tab change to save current engine
|
|
5601
|
+
self.web_search_notebook.bind("<<NotebookTabChanged>>", self._on_web_search_tab_changed)
|
|
5602
|
+
|
|
5603
|
+
# Select previously active tab
|
|
5604
|
+
active_engine = self.settings["tool_settings"].get("Web Search", {}).get("active_engine", "duckduckgo")
|
|
5605
|
+
for i, (_, key, _, _, _) in enumerate(self.web_search_engines):
|
|
5606
|
+
if key == active_engine:
|
|
5607
|
+
self.web_search_notebook.select(i)
|
|
5608
|
+
break
|
|
5609
|
+
|
|
5610
|
+
def _load_web_search_api_key(self, engine_key):
|
|
5611
|
+
"""Load encrypted API key for a search engine."""
|
|
5612
|
+
try:
|
|
5613
|
+
from tools.ai_tools import decrypt_api_key
|
|
5614
|
+
encrypted = self.settings["tool_settings"].get("Web Search", {}).get(f"{engine_key}_api_key", "")
|
|
5615
|
+
if encrypted:
|
|
5616
|
+
return decrypt_api_key(encrypted)
|
|
5617
|
+
except Exception:
|
|
5618
|
+
pass
|
|
5619
|
+
return ""
|
|
5620
|
+
|
|
5621
|
+
def _save_web_search_api_key(self, engine_key, api_key):
|
|
5622
|
+
"""Save encrypted API key for a search engine."""
|
|
5623
|
+
try:
|
|
5624
|
+
from tools.ai_tools import encrypt_api_key
|
|
5625
|
+
if "Web Search" not in self.settings["tool_settings"]:
|
|
5626
|
+
self.settings["tool_settings"]["Web Search"] = {}
|
|
5627
|
+
if api_key:
|
|
5628
|
+
self.settings["tool_settings"]["Web Search"][f"{engine_key}_api_key"] = encrypt_api_key(api_key)
|
|
5629
|
+
else:
|
|
5630
|
+
self.settings["tool_settings"]["Web Search"].pop(f"{engine_key}_api_key", None)
|
|
5631
|
+
self.save_settings()
|
|
5632
|
+
except Exception as e:
|
|
5633
|
+
self.logger.error(f"Failed to save API key: {e}")
|
|
5634
|
+
|
|
5635
|
+
def _get_web_search_setting(self, engine_key, setting, default):
|
|
5636
|
+
"""Get a search engine setting."""
|
|
5637
|
+
return self.settings["tool_settings"].get("Web Search", {}).get(f"{engine_key}_{setting}", default)
|
|
5638
|
+
|
|
5639
|
+
def _save_web_search_setting(self, engine_key, setting, value):
|
|
5640
|
+
"""Save a search engine setting."""
|
|
5641
|
+
if "Web Search" not in self.settings["tool_settings"]:
|
|
5642
|
+
self.settings["tool_settings"]["Web Search"] = {}
|
|
5643
|
+
self.settings["tool_settings"]["Web Search"][f"{engine_key}_{setting}"] = value
|
|
5644
|
+
self.save_settings()
|
|
5645
|
+
|
|
5646
|
+
def _on_web_search_tab_changed(self, event=None):
|
|
5647
|
+
"""Save the currently active search engine tab."""
|
|
5648
|
+
if hasattr(self, 'web_search_notebook'):
|
|
5649
|
+
idx = self.web_search_notebook.index(self.web_search_notebook.select())
|
|
5650
|
+
if idx < len(self.web_search_engines):
|
|
5651
|
+
engine_key = self.web_search_engines[idx][1]
|
|
5652
|
+
if "Web Search" not in self.settings["tool_settings"]:
|
|
5653
|
+
self.settings["tool_settings"]["Web Search"] = {}
|
|
5654
|
+
self.settings["tool_settings"]["Web Search"]["active_engine"] = engine_key
|
|
5655
|
+
self.save_settings()
|
|
5656
|
+
|
|
5657
|
+
def _do_web_search(self, engine_key):
|
|
5658
|
+
"""Perform web search using the selected engine with inline implementations."""
|
|
5659
|
+
# Get input text
|
|
5660
|
+
current_tab_index = self.input_notebook.index(self.input_notebook.select())
|
|
5661
|
+
active_input_tab = self.input_tabs[current_tab_index]
|
|
5662
|
+
query = active_input_tab.text.get("1.0", tk.END).strip()
|
|
5663
|
+
|
|
5664
|
+
if not query:
|
|
5665
|
+
self.update_output_text("Please enter a search query in the Input panel.")
|
|
5666
|
+
return
|
|
5667
|
+
|
|
5668
|
+
# Get settings
|
|
5669
|
+
count = int(self.web_search_count_vars.get(engine_key, tk.StringVar(value="5")).get())
|
|
5670
|
+
|
|
5671
|
+
try:
|
|
5672
|
+
# Inline search based on engine - uses API keys from settings
|
|
5673
|
+
if engine_key == "duckduckgo":
|
|
5674
|
+
results = self._search_duckduckgo(query, count)
|
|
5675
|
+
elif engine_key == "tavily":
|
|
5676
|
+
results = self._search_tavily(query, count)
|
|
5677
|
+
elif engine_key == "google":
|
|
5678
|
+
results = self._search_google(query, count)
|
|
5679
|
+
elif engine_key == "brave":
|
|
5680
|
+
results = self._search_brave(query, count)
|
|
5681
|
+
elif engine_key == "serpapi":
|
|
5682
|
+
results = self._search_serpapi(query, count)
|
|
5683
|
+
elif engine_key == "serper":
|
|
5684
|
+
results = self._search_serper(query, count)
|
|
5685
|
+
else:
|
|
5686
|
+
self.update_output_text(f"Unknown search engine: {engine_key}")
|
|
5687
|
+
return
|
|
5688
|
+
|
|
5689
|
+
if not results:
|
|
5690
|
+
self.update_output_text(f"No results found for '{query}'")
|
|
5691
|
+
return
|
|
5692
|
+
|
|
5693
|
+
# Format results
|
|
5694
|
+
lines = [f"Search Results ({engine_key.upper()}):", "=" * 50, ""]
|
|
5695
|
+
for i, r in enumerate(results, 1):
|
|
5696
|
+
lines.append(f"{i}. {r.get('title', 'No title')}")
|
|
5697
|
+
snippet = r.get('snippet', '')[:200]
|
|
5698
|
+
if len(r.get('snippet', '')) > 200:
|
|
5699
|
+
snippet += "..."
|
|
5700
|
+
lines.append(f" {snippet}")
|
|
5701
|
+
lines.append(f" URL: {r.get('url', 'N/A')}")
|
|
5702
|
+
lines.append("")
|
|
5703
|
+
|
|
5704
|
+
self.update_output_text("\n".join(lines))
|
|
5705
|
+
except Exception as e:
|
|
5706
|
+
self.update_output_text(f"Search error: {str(e)}")
|
|
5707
|
+
|
|
5708
|
+
def _search_duckduckgo(self, query: str, count: int) -> list:
|
|
5709
|
+
"""Search DuckDuckGo using ddgs package (free, no API key)."""
|
|
5710
|
+
try:
|
|
5711
|
+
from ddgs import DDGS
|
|
5712
|
+
except ImportError:
|
|
5713
|
+
return [{"title": "Error", "snippet": "DuckDuckGo requires: pip install ddgs", "url": ""}]
|
|
5714
|
+
|
|
5715
|
+
try:
|
|
5716
|
+
with DDGS() as ddgs:
|
|
5717
|
+
results = []
|
|
5718
|
+
for r in ddgs.text(query, max_results=count):
|
|
5719
|
+
results.append({
|
|
5720
|
+
"title": r.get("title", ""),
|
|
5721
|
+
"snippet": r.get("body", ""),
|
|
5722
|
+
"url": r.get("href", ""),
|
|
5723
|
+
})
|
|
5724
|
+
return results
|
|
5725
|
+
except Exception as e:
|
|
5726
|
+
self.logger.error(f"DuckDuckGo search failed: {e}")
|
|
5727
|
+
return []
|
|
5728
|
+
|
|
5729
|
+
def _search_tavily(self, query: str, count: int) -> list:
|
|
5730
|
+
"""Search using Tavily API (AI-optimized search)."""
|
|
5731
|
+
api_key = self._load_web_search_api_key("tavily")
|
|
5732
|
+
if not api_key:
|
|
5733
|
+
return [{"title": "Error", "snippet": "Tavily API key required. Enter in API Configuration.", "url": ""}]
|
|
5734
|
+
|
|
5735
|
+
try:
|
|
5736
|
+
import urllib.request
|
|
5737
|
+
import json
|
|
5738
|
+
|
|
5739
|
+
url = "https://api.tavily.com/search"
|
|
5740
|
+
data = json.dumps({
|
|
5741
|
+
"api_key": api_key,
|
|
5742
|
+
"query": query,
|
|
5743
|
+
"search_depth": "basic",
|
|
5744
|
+
"max_results": count
|
|
5745
|
+
}).encode()
|
|
5746
|
+
|
|
5747
|
+
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
|
|
5748
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
5749
|
+
result = json.loads(response.read().decode())
|
|
5750
|
+
|
|
5751
|
+
results = []
|
|
5752
|
+
for item in result.get("results", []):
|
|
5753
|
+
results.append({
|
|
5754
|
+
"title": item.get("title", ""),
|
|
5755
|
+
"snippet": item.get("content", ""),
|
|
5756
|
+
"url": item.get("url", ""),
|
|
5757
|
+
})
|
|
5758
|
+
return results
|
|
5759
|
+
except Exception as e:
|
|
5760
|
+
self.logger.error(f"Tavily search failed: {e}")
|
|
5761
|
+
return [{"title": "Error", "snippet": str(e), "url": ""}]
|
|
5762
|
+
|
|
5763
|
+
def _search_google(self, query: str, count: int) -> list:
|
|
5764
|
+
"""Search using Google Custom Search API."""
|
|
5765
|
+
api_key = self._load_web_search_api_key("google")
|
|
5766
|
+
cse_id = self._get_web_search_setting("google", "cse_id", "")
|
|
5767
|
+
|
|
5768
|
+
if not api_key:
|
|
5769
|
+
return [{"title": "Error", "snippet": "Google API key required. Enter in API Configuration.", "url": ""}]
|
|
5770
|
+
if not cse_id:
|
|
5771
|
+
return [{"title": "Error", "snippet": "Google CSE ID required. Enter in API Configuration.", "url": ""}]
|
|
5772
|
+
|
|
5773
|
+
try:
|
|
5774
|
+
import urllib.request
|
|
5775
|
+
import urllib.parse
|
|
5776
|
+
import json
|
|
5777
|
+
|
|
5778
|
+
url = f"https://www.googleapis.com/customsearch/v1?key={api_key}&cx={cse_id}&q={urllib.parse.quote(query)}&num={min(count, 10)}"
|
|
5779
|
+
|
|
5780
|
+
req = urllib.request.Request(url)
|
|
5781
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
5782
|
+
data = json.loads(response.read().decode())
|
|
5783
|
+
|
|
5784
|
+
if "error" in data:
|
|
5785
|
+
return [{"title": "Error", "snippet": data["error"]["message"], "url": ""}]
|
|
5786
|
+
|
|
5787
|
+
results = []
|
|
5788
|
+
for item in data.get("items", []):
|
|
5789
|
+
results.append({
|
|
5790
|
+
"title": item.get("title", ""),
|
|
5791
|
+
"snippet": item.get("snippet", ""),
|
|
5792
|
+
"url": item.get("link", ""),
|
|
5793
|
+
})
|
|
5794
|
+
return results
|
|
5795
|
+
except Exception as e:
|
|
5796
|
+
self.logger.error(f"Google search failed: {e}")
|
|
5797
|
+
return [{"title": "Error", "snippet": str(e), "url": ""}]
|
|
5798
|
+
|
|
5799
|
+
def _search_brave(self, query: str, count: int) -> list:
|
|
5800
|
+
"""Search using Brave Search API."""
|
|
5801
|
+
api_key = self._load_web_search_api_key("brave")
|
|
5802
|
+
if not api_key:
|
|
5803
|
+
return [{"title": "Error", "snippet": "Brave API key required. Enter in API Configuration.", "url": ""}]
|
|
5804
|
+
|
|
5805
|
+
try:
|
|
5806
|
+
import urllib.request
|
|
5807
|
+
import urllib.parse
|
|
5808
|
+
import json
|
|
5809
|
+
|
|
5810
|
+
url = f"https://api.search.brave.com/res/v1/web/search?q={urllib.parse.quote(query)}&count={min(count, 20)}"
|
|
5811
|
+
|
|
5812
|
+
req = urllib.request.Request(url, headers={
|
|
5813
|
+
"Accept": "application/json",
|
|
5814
|
+
"X-Subscription-Token": api_key
|
|
5815
|
+
})
|
|
5816
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
5817
|
+
data = json.loads(response.read().decode())
|
|
5818
|
+
|
|
5819
|
+
results = []
|
|
5820
|
+
for item in data.get("web", {}).get("results", []):
|
|
5821
|
+
results.append({
|
|
5822
|
+
"title": item.get("title", ""),
|
|
5823
|
+
"snippet": item.get("description", ""),
|
|
5824
|
+
"url": item.get("url", ""),
|
|
5825
|
+
})
|
|
5826
|
+
return results
|
|
5827
|
+
except Exception as e:
|
|
5828
|
+
self.logger.error(f"Brave search failed: {e}")
|
|
5829
|
+
return [{"title": "Error", "snippet": str(e), "url": ""}]
|
|
5830
|
+
|
|
5831
|
+
def _search_serpapi(self, query: str, count: int) -> list:
|
|
5832
|
+
"""Search using SerpApi (Google SERP)."""
|
|
5833
|
+
api_key = self._load_web_search_api_key("serpapi")
|
|
5834
|
+
if not api_key:
|
|
5835
|
+
return [{"title": "Error", "snippet": "SerpApi key required. Enter in API Configuration.", "url": ""}]
|
|
5836
|
+
|
|
5837
|
+
try:
|
|
5838
|
+
import urllib.request
|
|
5839
|
+
import urllib.parse
|
|
5840
|
+
import json
|
|
5841
|
+
|
|
5842
|
+
url = f"https://serpapi.com/search?q={urllib.parse.quote(query)}&api_key={api_key}&num={min(count, 10)}"
|
|
5843
|
+
|
|
5844
|
+
req = urllib.request.Request(url)
|
|
5845
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
5846
|
+
data = json.loads(response.read().decode())
|
|
5847
|
+
|
|
5848
|
+
results = []
|
|
5849
|
+
for item in data.get("organic_results", []):
|
|
5850
|
+
results.append({
|
|
5851
|
+
"title": item.get("title", ""),
|
|
5852
|
+
"snippet": item.get("snippet", ""),
|
|
5853
|
+
"url": item.get("link", ""),
|
|
5854
|
+
})
|
|
5855
|
+
return results[:count]
|
|
5856
|
+
except Exception as e:
|
|
5857
|
+
self.logger.error(f"SerpApi search failed: {e}")
|
|
5858
|
+
return [{"title": "Error", "snippet": str(e), "url": ""}]
|
|
5859
|
+
|
|
5860
|
+
def _search_serper(self, query: str, count: int) -> list:
|
|
5861
|
+
"""Search using Serper.dev (Google SERP)."""
|
|
5862
|
+
api_key = self._load_web_search_api_key("serper")
|
|
5863
|
+
if not api_key:
|
|
5864
|
+
return [{"title": "Error", "snippet": "Serper API key required. Enter in API Configuration.", "url": ""}]
|
|
5865
|
+
|
|
5866
|
+
try:
|
|
5867
|
+
import urllib.request
|
|
5868
|
+
import json
|
|
5869
|
+
|
|
5870
|
+
url = "https://google.serper.dev/search"
|
|
5871
|
+
data = json.dumps({"q": query, "num": min(count, 10)}).encode()
|
|
5872
|
+
|
|
5873
|
+
req = urllib.request.Request(url, data=data, headers={
|
|
5874
|
+
"X-API-KEY": api_key,
|
|
5875
|
+
"Content-Type": "application/json"
|
|
5876
|
+
})
|
|
5877
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
5878
|
+
result = json.loads(response.read().decode())
|
|
5879
|
+
|
|
5880
|
+
results = []
|
|
5881
|
+
for item in result.get("organic", []):
|
|
5882
|
+
results.append({
|
|
5883
|
+
"title": item.get("title", ""),
|
|
5884
|
+
"snippet": item.get("snippet", ""),
|
|
5885
|
+
"url": item.get("link", ""),
|
|
5886
|
+
})
|
|
5887
|
+
return results
|
|
5888
|
+
except Exception as e:
|
|
5889
|
+
self.logger.error(f"Serper search failed: {e}")
|
|
5890
|
+
return [{"title": "Error", "snippet": str(e), "url": ""}]
|
|
5891
|
+
|
|
5892
|
+
def create_url_reader_options(self, parent):
|
|
5893
|
+
"""Creates the URL Reader tool options panel with format options."""
|
|
5894
|
+
main_frame = ttk.Frame(parent)
|
|
5895
|
+
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
5896
|
+
|
|
5897
|
+
# Options row
|
|
5898
|
+
options_frame = ttk.LabelFrame(main_frame, text="Output Format", padding=5)
|
|
5899
|
+
options_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
5900
|
+
|
|
5901
|
+
# Format selection
|
|
5902
|
+
self.url_reader_format_var = tk.StringVar(value=self.settings["tool_settings"].get("URL Reader", {}).get("format", "markdown"))
|
|
5903
|
+
|
|
5904
|
+
formats = [("Raw HTML", "html"), ("JSON", "json"), ("Markdown", "markdown")]
|
|
5905
|
+
for text, value in formats:
|
|
5906
|
+
rb = ttk.Radiobutton(options_frame, text=text, variable=self.url_reader_format_var, value=value)
|
|
5907
|
+
rb.pack(side=tk.LEFT, padx=10)
|
|
5908
|
+
|
|
5909
|
+
# Auto-save on change
|
|
5910
|
+
self.url_reader_format_var.trace_add("write", lambda *_: self._save_url_reader_settings())
|
|
5911
|
+
|
|
5912
|
+
# Action row
|
|
5913
|
+
action_frame = ttk.Frame(main_frame)
|
|
5914
|
+
action_frame.pack(fill=tk.X, padx=5, pady=10)
|
|
5915
|
+
|
|
5916
|
+
# Fetch button (becomes Cancel while fetching)
|
|
5917
|
+
self.url_fetch_btn = ttk.Button(action_frame, text="Fetch Content", command=self._toggle_url_fetch)
|
|
5918
|
+
self.url_fetch_btn.pack(side=tk.LEFT, padx=5)
|
|
5919
|
+
|
|
5920
|
+
# Status label
|
|
5921
|
+
self.url_fetch_status = ttk.Label(action_frame, text="Enter URLs in Input panel (one per line)")
|
|
5922
|
+
self.url_fetch_status.pack(side=tk.LEFT, padx=10)
|
|
5923
|
+
|
|
5924
|
+
# Track fetch state
|
|
5925
|
+
self.url_fetch_in_progress = False
|
|
5926
|
+
self.url_fetch_thread = None
|
|
5927
|
+
|
|
5928
|
+
def _save_url_reader_settings(self):
|
|
5929
|
+
"""Save URL Reader settings."""
|
|
5930
|
+
if "URL Reader" not in self.settings["tool_settings"]:
|
|
5931
|
+
self.settings["tool_settings"]["URL Reader"] = {}
|
|
5932
|
+
self.settings["tool_settings"]["URL Reader"]["format"] = self.url_reader_format_var.get()
|
|
5933
|
+
self.save_settings()
|
|
5934
|
+
|
|
5935
|
+
def _toggle_url_fetch(self):
|
|
5936
|
+
"""Toggle between Fetch Content and Cancel."""
|
|
5937
|
+
if self.url_fetch_in_progress:
|
|
5938
|
+
# Cancel the fetch
|
|
5939
|
+
self.url_fetch_in_progress = False
|
|
5940
|
+
self.url_fetch_btn.configure(text="Fetch Content")
|
|
5941
|
+
self.url_fetch_status.configure(text="Cancelled")
|
|
5942
|
+
else:
|
|
5943
|
+
# Start fetch
|
|
5944
|
+
self._start_url_fetch()
|
|
5945
|
+
|
|
5946
|
+
def _start_url_fetch(self):
|
|
5947
|
+
"""Start fetching URL content in background."""
|
|
5948
|
+
import threading
|
|
5949
|
+
|
|
5950
|
+
# Get URLs from input
|
|
5951
|
+
current_tab_index = self.input_notebook.index(self.input_notebook.select())
|
|
5952
|
+
active_input_tab = self.input_tabs[current_tab_index]
|
|
5953
|
+
text = active_input_tab.text.get("1.0", tk.END).strip()
|
|
5954
|
+
urls = [u.strip() for u in text.splitlines() if u.strip()]
|
|
5955
|
+
|
|
5956
|
+
if not urls:
|
|
5957
|
+
self.url_fetch_status.configure(text="No URLs found in Input panel")
|
|
5958
|
+
return
|
|
5959
|
+
|
|
5960
|
+
self.url_fetch_in_progress = True
|
|
5961
|
+
self.url_fetch_btn.configure(text="Cancel")
|
|
5962
|
+
self.url_fetch_status.configure(text=f"Fetching {len(urls)} URL(s)...")
|
|
5963
|
+
|
|
5964
|
+
output_format = self.url_reader_format_var.get()
|
|
5965
|
+
|
|
5966
|
+
def fetch_worker():
|
|
5967
|
+
from tools.url_content_reader import URLContentReader
|
|
5968
|
+
reader = URLContentReader()
|
|
5969
|
+
results = []
|
|
5970
|
+
|
|
5971
|
+
for i, url in enumerate(urls):
|
|
5972
|
+
if not self.url_fetch_in_progress:
|
|
5973
|
+
results.append(f"# Cancelled\n\nFetch cancelled by user.")
|
|
5974
|
+
break
|
|
5975
|
+
|
|
5976
|
+
self.after(0, lambda i=i, t=len(urls): self.url_fetch_status.configure(text=f"Fetching {i+1}/{t}..."))
|
|
5977
|
+
|
|
5978
|
+
try:
|
|
5979
|
+
if output_format == "html":
|
|
5980
|
+
content = reader.fetch_url(url, timeout=30)
|
|
5981
|
+
results.append(f"<!-- URL: {url} -->\n{content}")
|
|
5982
|
+
elif output_format == "json":
|
|
5983
|
+
import json
|
|
5984
|
+
content = reader.fetch_url(url, timeout=30)
|
|
5985
|
+
results.append(json.dumps({"url": url, "html": content[:5000]}, indent=2))
|
|
5986
|
+
else: # markdown
|
|
5987
|
+
content = reader.fetch_and_convert(url, timeout=30)
|
|
5988
|
+
results.append(f"# Content from: {url}\n\n{content}")
|
|
5989
|
+
except Exception as e:
|
|
5990
|
+
results.append(f"# Error: {url}\n\nError: {str(e)}")
|
|
5991
|
+
|
|
5992
|
+
final_output = "\n\n---\n\n".join(results)
|
|
5993
|
+
|
|
5994
|
+
def update_ui():
|
|
5995
|
+
self.url_fetch_in_progress = False
|
|
5996
|
+
self.url_fetch_btn.configure(text="Fetch Content")
|
|
5997
|
+
self.url_fetch_status.configure(text="Complete")
|
|
5998
|
+
self.update_output_text(final_output)
|
|
5999
|
+
|
|
6000
|
+
self.after(0, update_ui)
|
|
6001
|
+
|
|
6002
|
+
self.url_fetch_thread = threading.Thread(target=fetch_worker, daemon=True)
|
|
6003
|
+
self.url_fetch_thread.start()
|
|
6004
|
+
|
|
5321
6005
|
def create_html_extraction_tool_widget(self, parent):
|
|
5322
6006
|
"""Create and configure the HTML Extraction Tool widget."""
|
|
5323
6007
|
if not HTML_EXTRACTION_TOOL_MODULE_AVAILABLE:
|
|
@@ -6687,6 +7371,51 @@ class PromeraAIApp(tk.Tk):
|
|
|
6687
7371
|
return self.base64_tools.process_text(input_text, settings)
|
|
6688
7372
|
else:
|
|
6689
7373
|
return "Base64 Tools module not available"
|
|
7374
|
+
elif tool_name == "Web Search":
|
|
7375
|
+
try:
|
|
7376
|
+
from tools.web_search import search
|
|
7377
|
+
settings = self.settings["tool_settings"].get("Web Search", {})
|
|
7378
|
+
engine = settings.get("engine", "duckduckgo")
|
|
7379
|
+
count = settings.get("count", 5)
|
|
7380
|
+
query = input_text.strip()
|
|
7381
|
+
if not query:
|
|
7382
|
+
return "Please enter a search query in the input panel."
|
|
7383
|
+
results = search(query, engine, count)
|
|
7384
|
+
if not results:
|
|
7385
|
+
return f"No results found for '{query}'"
|
|
7386
|
+
lines = [f"Search Results ({engine}):", "=" * 50, ""]
|
|
7387
|
+
for i, r in enumerate(results, 1):
|
|
7388
|
+
lines.append(f"{i}. {r.get('title', 'No title')}")
|
|
7389
|
+
snippet = r.get('snippet', '')[:200]
|
|
7390
|
+
if len(r.get('snippet', '')) > 200:
|
|
7391
|
+
snippet += "..."
|
|
7392
|
+
lines.append(f" {snippet}")
|
|
7393
|
+
lines.append(f" URL: {r.get('url', 'N/A')}")
|
|
7394
|
+
lines.append("")
|
|
7395
|
+
return "\n".join(lines)
|
|
7396
|
+
except ImportError:
|
|
7397
|
+
return "Web Search module not available"
|
|
7398
|
+
except Exception as e:
|
|
7399
|
+
return f"Search error: {str(e)}"
|
|
7400
|
+
elif tool_name == "URL Reader":
|
|
7401
|
+
try:
|
|
7402
|
+
from tools.url_content_reader import URLContentReader
|
|
7403
|
+
reader = URLContentReader()
|
|
7404
|
+
urls = [u.strip() for u in input_text.strip().splitlines() if u.strip()]
|
|
7405
|
+
if not urls:
|
|
7406
|
+
return "Please enter one or more URLs in the input panel (one per line)."
|
|
7407
|
+
all_output = []
|
|
7408
|
+
for url in urls:
|
|
7409
|
+
try:
|
|
7410
|
+
markdown = reader.fetch_and_convert(url, timeout=30)
|
|
7411
|
+
all_output.append(f"# Content from: {url}\n\n{markdown}\n\n---\n")
|
|
7412
|
+
except Exception as e:
|
|
7413
|
+
all_output.append(f"# Error fetching: {url}\n\nError: {str(e)}\n\n---\n")
|
|
7414
|
+
return "\n".join(all_output)
|
|
7415
|
+
except ImportError:
|
|
7416
|
+
return "URL Content Reader module not available"
|
|
7417
|
+
except Exception as e:
|
|
7418
|
+
return f"URL Reader error: {str(e)}"
|
|
6690
7419
|
else:
|
|
6691
7420
|
return f"Unknown tool: {tool_name}"
|
|
6692
7421
|
|
|
@@ -7349,7 +8078,7 @@ class PromeraAIApp(tk.Tk):
|
|
|
7349
8078
|
return
|
|
7350
8079
|
|
|
7351
8080
|
try:
|
|
7352
|
-
with open(filename, 'r') as f:
|
|
8081
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
|
7353
8082
|
import_data = json.load(f)
|
|
7354
8083
|
|
|
7355
8084
|
file_size = os.path.getsize(filename)
|
|
@@ -7403,7 +8132,13 @@ class PromeraAIApp(tk.Tk):
|
|
|
7403
8132
|
current_settings = self.db_settings_manager.load_settings()
|
|
7404
8133
|
current_tools = len(current_settings.get("tool_settings", {}))
|
|
7405
8134
|
|
|
7406
|
-
|
|
8135
|
+
# Reload settings into UI to apply imported tab content
|
|
8136
|
+
self.settings = current_settings
|
|
8137
|
+
self.load_last_state()
|
|
8138
|
+
self.update_tab_labels()
|
|
8139
|
+
self.logger.info("Imported settings loaded into UI")
|
|
8140
|
+
|
|
8141
|
+
success_msg = f"Settings imported successfully!\n\nFrom: {os.path.basename(filename)}\nTool configurations: {current_tools}\n\nTab content has been refreshed."
|
|
7407
8142
|
self.show_success(success_msg)
|
|
7408
8143
|
self.logger.info(f"Import successful - {current_tools} tools now in database")
|
|
7409
8144
|
except Exception as verify_error:
|