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/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
- if "pattern_library" not in self.app.settings:
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
- self.app.logger.info(f"Loaded comprehensive pattern library with {len(self.app.settings['pattern_library'])} patterns")
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
- # Force a backup to disk
1880
- self.db_settings_manager.connection_manager.backup_to_disk()
1881
- self.logger.info("Settings saved to database")
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
- memory_options = [
4491
- ("GC optimization", "gc_optimization", True),
4492
- ("Memory pool", "memory_pool", True),
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
- success_msg = f"Settings imported successfully!\n\nFrom: {os.path.basename(filename)}\nTool configurations: {current_tools}\n\nPlease restart the application for all changes to take effect."
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: