pomera-ai-commander 1.2.8 → 1.2.10

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"
@@ -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
- # Force a backup to disk
1882
- self.db_settings_manager.connection_manager.backup_to_disk()
1883
- 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)")
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
- memory_options = [
4493
- ("GC optimization", "gc_optimization", True),
4494
- ("Memory pool", "memory_pool", True),
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
- 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."
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: