pomera-ai-commander 1.2.8 → 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/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/mcp.json +1 -1
- package/package.json +1 -1
- package/pomera.py +755 -22
- package/tools/case_tool.py +4 -4
- package/tools/curl_settings.py +12 -1
- package/tools/curl_tool.py +176 -11
- package/tools/tool_loader.py +18 -0
- 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"
|
|
@@ -789,6 +864,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
789
864
|
global DATABASE_SETTINGS_AVAILABLE
|
|
790
865
|
super().__init__()
|
|
791
866
|
|
|
867
|
+
# Start profiling from here
|
|
868
|
+
_startup_profiler.start("Database/Settings Init")
|
|
869
|
+
|
|
792
870
|
# Record startup time for performance monitoring
|
|
793
871
|
self.startup_time = time.time()
|
|
794
872
|
|
|
@@ -821,7 +899,8 @@ class PromeraAIApp(tk.Tk):
|
|
|
821
899
|
"Base64 Encoder/Decoder", "Translator Tools", "Diff Viewer",
|
|
822
900
|
"Line Tools", "Whitespace Tools", "Text Statistics", "Markdown Tools",
|
|
823
901
|
"String Escape Tool", "Number Base Converter", "Text Wrapper",
|
|
824
|
-
"Column Tools", "Timestamp Converter"
|
|
902
|
+
"Column Tools", "Timestamp Converter",
|
|
903
|
+
"Web Search", "URL Reader" # Manual search/fetch only on button click
|
|
825
904
|
]
|
|
826
905
|
|
|
827
906
|
# CORRECTED ORDER: Load settings BEFORE setting up logging
|
|
@@ -866,9 +945,14 @@ class PromeraAIApp(tk.Tk):
|
|
|
866
945
|
self.settings = self.load_settings()
|
|
867
946
|
else:
|
|
868
947
|
self.settings = self.load_settings()
|
|
948
|
+
_startup_profiler.end("Database/Settings Init")
|
|
949
|
+
|
|
950
|
+
_startup_profiler.start("Logging/Audio Setup")
|
|
869
951
|
self.setup_logging()
|
|
870
952
|
self.setup_audio()
|
|
953
|
+
_startup_profiler.end("Logging/Audio Setup")
|
|
871
954
|
|
|
955
|
+
_startup_profiler.start("DialogManager Init")
|
|
872
956
|
# Initialize DialogManager BEFORE optimized components
|
|
873
957
|
if DIALOG_MANAGER_AVAILABLE:
|
|
874
958
|
self.dialog_settings_adapter = DialogSettingsAdapter(self)
|
|
@@ -927,10 +1011,19 @@ class PromeraAIApp(tk.Tk):
|
|
|
927
1011
|
self.logger.warning("Tool Loader not available")
|
|
928
1012
|
|
|
929
1013
|
# Setup optimized components AFTER DialogManager is available
|
|
1014
|
+
_startup_profiler.end("DialogManager Init")
|
|
1015
|
+
|
|
1016
|
+
_startup_profiler.start("Optimized Components")
|
|
930
1017
|
self.setup_optimized_components()
|
|
1018
|
+
_startup_profiler.end("Optimized Components")
|
|
931
1019
|
|
|
1020
|
+
_startup_profiler.start("Create Widgets")
|
|
932
1021
|
self.create_widgets()
|
|
1022
|
+
_startup_profiler.end("Create Widgets")
|
|
1023
|
+
|
|
1024
|
+
_startup_profiler.start("Load Last State")
|
|
933
1025
|
self.load_last_state()
|
|
1026
|
+
_startup_profiler.end("Load Last State")
|
|
934
1027
|
|
|
935
1028
|
# Initialization complete - allow automatic processing
|
|
936
1029
|
self._initializing = False
|
|
@@ -989,6 +1082,11 @@ class PromeraAIApp(tk.Tk):
|
|
|
989
1082
|
self.bind_all("<Control-Shift-h>", self.toggle_options_panel)
|
|
990
1083
|
self.bind_all("<Control-Shift-H>", self.toggle_options_panel) # Windows needs uppercase
|
|
991
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
|
+
|
|
992
1090
|
# Set up window focus and minimize event handlers for visibility-aware updates
|
|
993
1091
|
if hasattr(self, 'statistics_update_manager') and self.statistics_update_manager:
|
|
994
1092
|
self.bind("<FocusIn>", self._on_window_focus_in)
|
|
@@ -1008,6 +1106,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
1008
1106
|
|
|
1009
1107
|
# Schedule background maintenance tasks
|
|
1010
1108
|
self._schedule_maintenance_tasks()
|
|
1109
|
+
|
|
1110
|
+
# Print startup profiling report
|
|
1111
|
+
_startup_profiler.summary()
|
|
1011
1112
|
|
|
1012
1113
|
def _schedule_maintenance_tasks(self):
|
|
1013
1114
|
"""Schedule periodic background maintenance tasks using Task Scheduler."""
|
|
@@ -1052,6 +1153,50 @@ class PromeraAIApp(tk.Tk):
|
|
|
1052
1153
|
|
|
1053
1154
|
self.logger.info("Background maintenance tasks scheduled (including auto-backup)")
|
|
1054
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
|
+
|
|
1055
1200
|
def _show_data_location_dialog(self):
|
|
1056
1201
|
"""Show the Data Location settings dialog."""
|
|
1057
1202
|
from tkinter import filedialog
|
|
@@ -1869,6 +2014,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
1869
2014
|
self.settings["active_input_tab"] = self.input_notebook.index(self.input_notebook.select())
|
|
1870
2015
|
self.settings["active_output_tab"] = self.output_notebook.index(self.output_notebook.select())
|
|
1871
2016
|
|
|
2017
|
+
# Save current tool selection (including Diff Viewer)
|
|
2018
|
+
self.settings["selected_tool"] = self.tool_var.get()
|
|
2019
|
+
|
|
1872
2020
|
# Debug logging to track what's being saved
|
|
1873
2021
|
non_empty_inputs = sum(1 for content in input_contents if content)
|
|
1874
2022
|
non_empty_outputs = sum(1 for content in output_contents if content)
|
|
@@ -1878,9 +2026,19 @@ class PromeraAIApp(tk.Tk):
|
|
|
1878
2026
|
if hasattr(self, 'db_settings_manager') and self.db_settings_manager:
|
|
1879
2027
|
try:
|
|
1880
2028
|
# Database manager handles saving automatically through the proxy
|
|
1881
|
-
#
|
|
1882
|
-
|
|
1883
|
-
|
|
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)")
|
|
1884
2042
|
except Exception as e:
|
|
1885
2043
|
self._handle_error(e, "Saving to database", "Settings",
|
|
1886
2044
|
user_message="Failed to save settings to database, trying JSON fallback",
|
|
@@ -3082,6 +3240,10 @@ class PromeraAIApp(tk.Tk):
|
|
|
3082
3240
|
|
|
3083
3241
|
file_menu.add_separator()
|
|
3084
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
|
+
|
|
3085
3247
|
# Settings Backup and Recovery submenu
|
|
3086
3248
|
if DATABASE_SETTINGS_AVAILABLE and hasattr(self, 'db_settings_manager'):
|
|
3087
3249
|
backup_menu = tk.Menu(file_menu, tearoff=0)
|
|
@@ -3808,7 +3970,32 @@ class PromeraAIApp(tk.Tk):
|
|
|
3808
3970
|
else:
|
|
3809
3971
|
self.widget_cache = None
|
|
3810
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
|
|
3811
3997
|
self.update_tool_settings_ui()
|
|
3998
|
+
self.logger.info("Deferred tool settings UI initialization complete")
|
|
3812
3999
|
|
|
3813
4000
|
def _create_tool_search_palette(self, parent):
|
|
3814
4001
|
"""Create the new ToolSearchPalette for tool selection."""
|
|
@@ -4489,17 +4676,9 @@ class PromeraAIApp(tk.Tk):
|
|
|
4489
4676
|
ttk.Checkbutton(memory_frame, text="Enable advanced memory management",
|
|
4490
4677
|
variable=self.memory_enabled_var).pack(anchor=tk.W)
|
|
4491
4678
|
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
("Memory leak detection", "leak_detection", True)
|
|
4496
|
-
]
|
|
4497
|
-
|
|
4498
|
-
self.memory_option_vars = {}
|
|
4499
|
-
for text, key, default in memory_options:
|
|
4500
|
-
var = tk.BooleanVar(value=memory_settings.get(key, default))
|
|
4501
|
-
self.memory_option_vars[key] = var
|
|
4502
|
-
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
|
|
4503
4682
|
|
|
4504
4683
|
# Memory threshold
|
|
4505
4684
|
mem_threshold_frame = ttk.Frame(memory_frame)
|
|
@@ -4592,9 +4771,6 @@ class PromeraAIApp(tk.Tk):
|
|
|
4592
4771
|
},
|
|
4593
4772
|
"memory_management": {
|
|
4594
4773
|
"enabled": self.memory_enabled_var.get(),
|
|
4595
|
-
"gc_optimization": self.memory_option_vars["gc_optimization"].get(),
|
|
4596
|
-
"memory_pool": self.memory_option_vars["memory_pool"].get(),
|
|
4597
|
-
"leak_detection": self.memory_option_vars["leak_detection"].get(),
|
|
4598
4774
|
"memory_threshold_mb": int(self.memory_threshold_var.get())
|
|
4599
4775
|
},
|
|
4600
4776
|
"ui_optimizations": {
|
|
@@ -5199,8 +5375,10 @@ class PromeraAIApp(tk.Tk):
|
|
|
5199
5375
|
self.create_column_tools_widget(parent_frame)
|
|
5200
5376
|
elif tool_name == "Timestamp Converter":
|
|
5201
5377
|
self.create_timestamp_converter_widget(parent_frame)
|
|
5202
|
-
|
|
5203
|
-
|
|
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)
|
|
5204
5382
|
|
|
5205
5383
|
|
|
5206
5384
|
|
|
@@ -5320,6 +5498,510 @@ class PromeraAIApp(tk.Tk):
|
|
|
5320
5498
|
except:
|
|
5321
5499
|
pass # Continue if font application fails
|
|
5322
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
|
+
|
|
5323
6005
|
def create_html_extraction_tool_widget(self, parent):
|
|
5324
6006
|
"""Create and configure the HTML Extraction Tool widget."""
|
|
5325
6007
|
if not HTML_EXTRACTION_TOOL_MODULE_AVAILABLE:
|
|
@@ -6689,6 +7371,51 @@ class PromeraAIApp(tk.Tk):
|
|
|
6689
7371
|
return self.base64_tools.process_text(input_text, settings)
|
|
6690
7372
|
else:
|
|
6691
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)}"
|
|
6692
7419
|
else:
|
|
6693
7420
|
return f"Unknown tool: {tool_name}"
|
|
6694
7421
|
|
|
@@ -7351,7 +8078,7 @@ class PromeraAIApp(tk.Tk):
|
|
|
7351
8078
|
return
|
|
7352
8079
|
|
|
7353
8080
|
try:
|
|
7354
|
-
with open(filename, 'r') as f:
|
|
8081
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
|
7355
8082
|
import_data = json.load(f)
|
|
7356
8083
|
|
|
7357
8084
|
file_size = os.path.getsize(filename)
|
|
@@ -7405,7 +8132,13 @@ class PromeraAIApp(tk.Tk):
|
|
|
7405
8132
|
current_settings = self.db_settings_manager.load_settings()
|
|
7406
8133
|
current_tools = len(current_settings.get("tool_settings", {}))
|
|
7407
8134
|
|
|
7408
|
-
|
|
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."
|
|
7409
8142
|
self.show_success(success_msg)
|
|
7410
8143
|
self.logger.info(f"Import successful - {current_tools} tools now in database")
|
|
7411
8144
|
except Exception as verify_error:
|