| import sys |
| import os |
| import json |
| import logging |
| import asyncio |
| import glob |
| import time |
| from typing import List, Dict, Any, Optional, Tuple |
| import gradio as gr |
| from pathlib import Path |
| from src.browser.custom_context import CustomBrowserContext |
|
|
| logger = logging.getLogger(__name__) |
| logger.debug(f"USER_INPUT_FUNCTIONS: Imported CustomBrowserContext class id: {id(CustomBrowserContext)}") |
|
|
| |
| _browser_context = None |
|
|
| |
| |
| |
|
|
| def set_browser_context(ctx): |
| """Called by webui after creating or re‑using a CustomBrowserContext.""" |
| global _browser_context |
| _browser_context = ctx |
| logger.debug("Browser context set in user_input_functions: %s", ctx) |
| if _browser_context: |
| logger.debug(f"USER_INPUT_FUNCTIONS: _browser_context object type id in set_browser_context: {id(type(_browser_context))}") |
|
|
| def list_input_trace_files(directory_path_str: str) -> List[dict]: |
| """Lists input trace files from the specified directory.""" |
| files_info = [] |
| try: |
| directory = Path(directory_path_str) |
| if not directory.is_dir(): |
| logger.warning(f"Directory not found or not a directory: {directory_path_str}") |
| return files_info |
|
|
| for fp in sorted(directory.glob("*.jsonl")): |
| try: |
| size_kb = fp.stat().st_size / 1024 |
| event_count = 0 |
| with fp.open('r') as f: |
| for _ in f: |
| event_count += 1 |
| files_info.append({ |
| "path": str(fp), |
| "name": fp.name, |
| "created": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(fp.stat().st_ctime)), |
| "size": f"{size_kb:.1f} KB", |
| "events": event_count, |
| }) |
| except Exception as e: |
| logger.debug("Skipping file %s due to error: %s", fp, e) |
| except Exception as e: |
| logger.error(f"Error listing trace files in {directory_path_str}: {e}") |
| return files_info |
|
|
| async def replay_input_trace(path: str, speed: float = 1.0) -> bool: |
| """Entry point used by webui's 'Replay Selected Trace' button.""" |
| logger.debug("Entered user_input_functions.replay_input_trace – Path: %s", path) |
| logger.debug(f"user_input_functions: Imported CustomBrowserContext class id: {id(CustomBrowserContext)}") |
| if _browser_context is None: |
| logger.error("No browser context set before replay_input_trace call") |
| return False |
| try: |
| logger.debug(f"user_input_functions: _browser_context object is: {repr(_browser_context)}") |
| logger.debug(f"user_input_functions: _browser_context object type is: {type(_browser_context)}") |
| logger.debug(f"user_input_functions: _browser_context object's class id: {id(type(_browser_context))}") |
| if not isinstance(_browser_context, CustomBrowserContext): |
| logger.error(f"Browser context is not a CustomBrowserContext instance: {type(_browser_context)}") |
| return False |
|
|
| ok = await _browser_context.replay_input_events(path, speed=speed, keep_open=True) |
| return ok |
| except Exception as e: |
| logger.exception("Replay failed during replay_input_trace for path %s: %s", path, e) |
| return False |
|
|
| async def start_input_tracking() -> tuple[str, bool, str]: |
| """ |
| Start tracking user input events. |
| |
| Returns: |
| Tuple[str, bool, str]: Status message, enable/disable for tracking button, path for file selection |
| """ |
| global _browser_context |
| |
| if not _browser_context: |
| return "No active browser session. Please start a browser session first.", False, "" |
| |
| try: |
| logger.debug(f"USER_INPUT_FUNCTIONS: In start_input_tracking - _browser_context object type id: {id(type(_browser_context))}") |
| logger.debug(f"USER_INPUT_FUNCTIONS: In start_input_tracking - CustomBrowserContext class id: {id(CustomBrowserContext)}") |
| |
| if not isinstance(_browser_context, CustomBrowserContext): |
| logger.error(f"Cannot start tracking: _browser_context is not a CustomBrowserContext: {type(_browser_context)}") |
| return "Internal error: Browser context not configured correctly.", False, "" |
|
|
| await _browser_context.start_input_tracking() |
| return "User input tracking initiated successfully.", True, "" |
| except Exception as e: |
| logger.error(f"Error calling _browser_context.start_input_tracking: {str(e)}", exc_info=True) |
| return f"Error: {str(e)}", False, "" |
|
|
| async def stop_input_tracking() -> tuple[str, Dict[str, Any], Dict[str, Any], str | None, Dict[str, Any] | None]: |
| """ |
| Stop tracking user input events. |
| |
| Returns: |
| Tuple for Gradio output: Status message, start_btn_update, stop_btn_update, trace_file_path, trace_info_json |
| """ |
| global _browser_context |
| |
| start_btn_update = gr.update(value="▶️ Start Recording", interactive=True) |
| stop_btn_update = gr.update(value="⏹️ Stop Recording", interactive=False) |
| default_trace_info = {"message": "No trace information available."} |
|
|
| if not _browser_context: |
| return "No active browser session.", start_btn_update, stop_btn_update, None, default_trace_info |
| |
| try: |
| logger.debug(f"USER_INPUT_FUNCTIONS: In stop_input_tracking - _browser_context object type id: {id(type(_browser_context))}") |
| logger.debug(f"USER_INPUT_FUNCTIONS: In stop_input_tracking - CustomBrowserContext class id: {id(CustomBrowserContext)}") |
|
|
| if not isinstance(_browser_context, CustomBrowserContext): |
| logger.error(f"Cannot stop tracking: _browser_context is not a CustomBrowserContext: {type(_browser_context)}") |
| return "Internal error: Browser context not configured correctly.", start_btn_update, stop_btn_update, None, default_trace_info |
|
|
| filepath = await _browser_context.stop_input_tracking() |
| if filepath: |
| trace_info = get_file_info(filepath) |
| return f"Input tracking stopped. Trace saved to: {filepath}", start_btn_update, stop_btn_update, filepath, trace_info |
| else: |
| return "Input tracking stopped. No trace file was saved.", start_btn_update, stop_btn_update, None, default_trace_info |
| except Exception as e: |
| logger.error(f"Error calling _browser_context.stop_input_tracking: {str(e)}", exc_info=True) |
| return f"Error: {str(e)}", start_btn_update, stop_btn_update, None, default_trace_info |
|
|
| def get_file_info(trace_file_path: str) -> dict[str, Any]: |
| global _browser_context |
| try: |
| if not trace_file_path or not isinstance(trace_file_path, str): |
| return {"error": "Invalid or no trace file path provided."} |
|
|
| if not os.path.exists(trace_file_path): |
| return {"error": f"File not found: {trace_file_path}"} |
| |
| event_count = 0 |
| first_event_info = "No events found or could not read first event." |
| urls = set() |
| event_types_summary = {} |
|
|
| with open(trace_file_path, 'r') as f: |
| for i, line in enumerate(f): |
| if line.strip(): |
| event_count += 1 |
| try: |
| event = json.loads(line) |
| if i == 0: |
| ts = event.get("t", "N/A") |
| ev_type = event.get("type", "N/A") |
| first_event_info = f"First event (dt={ts}ms, type={ev_type})" |
| |
| |
| current_type = event.get("type", "unknown") |
| event_types_summary[current_type] = event_types_summary.get(current_type, 0) + 1 |
| |
| |
| if event.get("type") == "navigation" and "to" in event: |
| urls.add(event["to"]) |
| elif "url" in event: |
| urls.add(event["url"]) |
| |
| except json.JSONDecodeError: |
| if i == 0: |
| first_event_info = "First line is not valid JSON." |
| |
| pass |
|
|
| return { |
| "file_path": trace_file_path, |
| "total_events_counted": event_count, |
| "message": "Basic info loaded." |
| } |
| except Exception as e: |
| logger.error(f"Error getting file info for {trace_file_path}: {str(e)}") |
| return {"error": str(e), "file_path": trace_file_path} |