import json import os from datetime import datetime from pathlib import Path import html from collections import defaultdict # Import defaultdict import webbrowser # Define file paths (assuming they are in the same directory as the script) BASE_DIR = Path(__file__).resolve().parent SESSION_DATA_FILE = BASE_DIR / "session.jsonl" COLOR_CLASSES = ["teal", "green", "yellow", "red"] # For dynamic history item colors DASHBOARD_TEMPLATE_FILE = BASE_DIR / "dashboard.html" DASHBOARD_OUTPUT_FILE = BASE_DIR / "dashboard_generated.html" def format_timestamp(ts_str): """Formats an ISO timestamp string into a more readable format.""" if not ts_str: return "N/A" try: # Handle potential 'Z' for UTC if ts_str.endswith('Z'): ts_str = ts_str[:-1] + '+00:00' dt_obj = datetime.fromisoformat(ts_str) return dt_obj.strftime("%Y-%m-%d %H:%M:%S") except ValueError: return ts_str # Return original if parsing fails def format_duration(seconds): """Formats a duration in seconds into a human-readable string (e.g., 1m 38s).""" if seconds is None: return "N/A" try: s = int(seconds) if s < 0: return "N/A" m, s = divmod(s, 60) h, m = divmod(m, 60) if h > 0: return f"{h}h {m}m {s}s" elif m > 0: return f"{m}m {s}s" else: return f"{s}s" except (ValueError, TypeError): return "N/A" def escape_html(text): """Escapes HTML special characters in a string.""" if text is None: return "" return html.escape(str(text)) def read_session_data(filepath): """Reads session data from a JSONL file.""" data = [] if not filepath.exists(): print(f"Error: Session data file not found at {filepath}") return data with open(filepath, "r", encoding="utf-8") as f: for line in f: try: data.append(json.loads(line)) except json.JSONDecodeError as e: print(f"Error decoding JSON from line: {line.strip()} - {e}") return data def calculate_cost_by_model(all_data): """ Calculates the total estimated cost per model from all session data. """ cost_by_model = defaultdict(float) if not all_data: return dict(cost_by_model) for data in all_data: # Iterate through the models used summary for this interaction models_summary = data.get("models_used_summary", []) if not isinstance(models_summary, list): # print(f"Warning: 'models_used_summary' is not a list in data: {data}") # Optional debug continue for model_info in models_summary: if not isinstance(model_info, dict): # print(f"Warning: Item in 'models_used_summary' is not a dict in data: {data}") # Optional debug continue model_name = model_info.get("name", "Unknown Model") cost = model_info.get("cost", 0.0) # Ensure cost is a number before adding if isinstance(cost, (int, float)): cost_by_model[model_name] += cost else: print(f"Warning: Found non-numeric cost value for model '{model_name}': {cost} in data: {data}") return dict(cost_by_model) # Convert defaultdict to dict for final return def format_cost_by_model_html(cost_by_model): """Generates HTML list for cost breakdown by model.""" if not cost_by_model: return "" # Sort models by cost descending sorted_models = sorted(cost_by_model.items(), key=lambda item: item[1], reverse=True) list_items_html = "" for model, cost in sorted_models: list_items_html += f"""
  • {escape_html(model)}: ${cost:.4f}
  • """ return f"" def generate_stats_overview_html(all_data, cost_by_model): """Generates HTML for the main stats overview section (Total Cost + Cost by Model).""" total_estimated_cost = sum(item.get("token_summary", {}).get("estimated_cost", 0.0) or 0.0 for item in all_data) last_entry_timestamp_str = "N/A" if all_data: # Assuming all_data is sorted with newest entry last after reading last_interaction_data = all_data[-1] # Newest interaction last_entry_timestamp_str = format_timestamp(last_interaction_data.get("interaction_timestamp")) model_cost_list_html = format_cost_by_model_html(cost_by_model) return f"""
    ${total_estimated_cost:.4f}
    TOTAL ESTIMATED COST
    Last Entry: {escape_html(last_entry_timestamp_str)}

    COST BY MODEL

    {model_cost_list_html}
    """ def generate_secondary_stats_html(all_data): """Generates HTML for the secondary stats section (Tokens, Duration, Sessions).""" if not all_data: # Return the structure with N/A values if no data, matching dashboard.html's expectation return """
    0
    TOTAL PROMPT TOKENS
    0s
    TOTAL INTERACTION DURATION
    0
    TOTAL COMPLETION TOKENS
    0
    TOTAL SESSIONS
    """ total_duration_seconds = sum(item.get("interaction_duration_seconds", 0) or 0 for item in all_data) total_prompt_tokens = sum(item.get("token_summary", {}).get("prompt_tokens", 0) or 0 for item in all_data) total_completion_tokens = sum(item.get("token_summary", {}).get("completion_tokens", 0) or 0 for item in all_data) total_sessions = 0 if all_data: session_ids = set() for item in all_data: if item.get("session_id"): session_ids.add(item.get("session_id")) total_sessions = len(session_ids) formatted_duration = format_duration(total_duration_seconds) formatted_prompt_tokens = f"{total_prompt_tokens / 1_000_000:.2f}M" if total_prompt_tokens >= 1_000_000 else str(total_prompt_tokens) formatted_completion_tokens = f"{total_completion_tokens / 1_000_000:.2f}M" if total_completion_tokens >= 1_000_000 else str(total_completion_tokens) return f"""
    {formatted_prompt_tokens}
    TOTAL PROMPT TOKENS
    {formatted_duration}
    TOTAL INTERACTION DURATION
    {formatted_completion_tokens}
    TOTAL COMPLETION TOKENS
    {total_sessions}
    TOTAL SESSIONS
    """ def generate_collapsible_list_html(title, items_list): items_list = items_list or [] # Ensure items_list is not None if not items_list: return f"

    {escape_html(title)}: None

    " list_items_html = "".join(f"
  • {escape_html(item)}
  • " for item in items_list) return f"""
    {escape_html(title)} ({len(items_list)})
    """ def generate_token_summary_html(token_summary): token_summary = token_summary or {} # Ensure token_summary is not None if not token_summary: return "

    No token summary available.

    " return f"""
    Token Summary

    Prompt Tokens: {token_summary.get("prompt_tokens", "N/A")}

    Completion Tokens: {token_summary.get("completion_tokens", "N/A")}

    Total Tokens: {token_summary.get("total_tokens", "N/A")}

    Estimated Cost: ${token_summary.get("estimated_cost", 0.0):.6f}

    """ def generate_models_used_summary_html(models_summary): models_summary = models_summary or [] # Ensure models_summary is not None if not models_summary: return "

    No models used summary available.

    " rows_html = "" for model_info in models_summary: model_info = model_info or {} # Ensure model_info is not None rows_html += f""" {escape_html(model_info.get("name"))} {model_info.get("calls", "N/A")} ${model_info.get("cost", 0.0):.6f} {model_info.get("prompt_tokens", "N/A")} {model_info.get("completion_tokens", "N/A")} """ return f"""
    Models Used Summary ({len(models_summary)})
    {rows_html}
    Name Calls Cost Prompt Tokens Completion Tokens
    """ def generate_llm_calls_details_html(llm_calls): llm_calls = llm_calls or [] # Ensure llm_calls is not None if not llm_calls: return "

    No LLM call details available.

    " rows_html = "" for call in llm_calls: call = call or {} # Ensure call is not None rows_html += f""" {escape_html(call.get("model"))} {escape_html(call.get("id"))} {escape_html(call.get("finish_reason", "N/A"))} {call.get("prompt_tokens", "N/A")} {call.get("completion_tokens", "N/A")} ${call.get("cost", 0.0):.6f} {format_timestamp(call.get("timestamp"))} """ return f"""
    LLM Calls Details ({len(llm_calls)})
    {rows_html}
    Model ID Finish Reason Prompt Tokens Completion Tokens Cost Timestamp
    """ def generate_interaction_html(interaction_data, index, use_special_color_bar=False, special_color_class="blue"): """Generates HTML for a single interaction entry.""" interaction_data = interaction_data or {} session_id = escape_html(interaction_data.get("session_id", f"interaction-{index}")) project_name = escape_html(interaction_data.get("project_name", "N/A")) timestamp_str = format_timestamp(interaction_data.get("interaction_timestamp")) duration_str = format_duration(interaction_data.get("interaction_duration_seconds")) query_text = escape_html(interaction_data.get("query", "No query provided.")) aider_version = escape_html(interaction_data.get("aider_version", "N/A")) platform_info = escape_html(interaction_data.get("platform_info", "N/A")) python_version = escape_html(interaction_data.get("python_version", "N/A")) if use_special_color_bar: color_bar_class = special_color_class else: if COLOR_CLASSES: # Ensure COLOR_CLASSES is not empty color_bar_class = COLOR_CLASSES[index % len(COLOR_CLASSES)] else: color_bar_class = "teal" # Fallback if COLOR_CLASSES is somehow empty return f"""

    {project_name}

    {timestamp_str} (Duration: {duration_str})

    Query: {query_text}

    Session ID:
    {session_id}
    Aider Version:
    {aider_version} Platform: {platform_info}, Python: {python_version}
    Token Usage:
    {generate_token_summary_html(interaction_data.get("token_summary"))}
    Models Used:
    {generate_models_used_summary_html(interaction_data.get("models_used_summary"))}
    LLM Call Details:
    {generate_llm_calls_details_html(interaction_data.get("llm_calls_details"))}
    Modified Files (in chat context):
    {generate_collapsible_list_html("Modified Files in Chat", interaction_data.get("modified_files_in_chat"))}
    Commits Made This Interaction:
    {generate_collapsible_list_html("Commits Made This Interaction", interaction_data.get("commits_made_this_interaction"))}
    """ def main(): """Main function to generate the dashboard.""" all_session_data = read_session_data(SESSION_DATA_FILE) # Calculate cost by model once cost_by_model = calculate_cost_by_model(all_session_data) # Generate HTML for the different sections stats_overview_html = generate_stats_overview_html(all_session_data, cost_by_model) secondary_stats_html = generate_secondary_stats_html(all_session_data) latest_interaction_display_html = "" history_entries_html = "" project_name_header = "AIDER ANALYTICS" # Default if no data if not all_session_data: latest_interaction_display_html = '

    No latest interaction data to display.

    ' history_entries_html = '

    No interaction history to display.

    ' else: # Data is assumed to be oldest to newest from read_session_data data_for_processing = list(all_session_data) # Make a copy latest_interaction_data = data_for_processing.pop() # Removes and returns the last item (newest) project_name_header = escape_html(latest_interaction_data.get("project_name", "AIDER ANALYTICS")) # Get project name from latest interaction # Index 0 for latest, but color is overridden by use_special_color_bar latest_interaction_display_html = generate_interaction_html(latest_interaction_data, 0, use_special_color_bar=True, special_color_class="blue") history_entries_html_parts = [] if not data_for_processing: history_entries_html = '

    No further interaction history to display.

    ' else: # Iterate from newest to oldest for display for the rest of the history for i, interaction_data in enumerate(reversed(data_for_processing)): # i will be 0 for the newest in remaining, 1 for next, etc. history_entries_html_parts.append(generate_interaction_html(interaction_data, i)) history_entries_html = "\n".join(history_entries_html_parts) if not history_entries_html_parts: # Should not happen if data_for_processing was not empty history_entries_html = '

    No further interaction history to display.

    ' if not DASHBOARD_TEMPLATE_FILE.exists(): print(f"Error: Dashboard template file not found at {DASHBOARD_TEMPLATE_FILE}") # Create a basic HTML structure if template is missing, to show some output output_content = f""" Aider Analytics Dashboard

    {project_name_header} - Aider Analytics Dashboard

    Stats Overview

    {stats_overview_html}

    Secondary Stats

    {secondary_stats_html}

    Latest Interaction

    {latest_interaction_display_html}

    Interaction History

    {history_entries_html}

    Note: dashboard.html template was not found. This is a fallback display.

    """ else: with open(DASHBOARD_TEMPLATE_FILE, "r", encoding="utf-8") as f: template_content = f.read() output_content = template_content.replace("", project_name_header) output_content = output_content.replace("", stats_overview_html) output_content = output_content.replace("", secondary_stats_html) output_content = output_content.replace("", latest_interaction_display_html) output_content = output_content.replace("", history_entries_html) # Check if placeholders were correctly replaced (optional, for debugging) # if "" in output_content and "" not in stats_overview_html: # print("Warning: Stats overview placeholder was not replaced.") # if "" in output_content and "" not in secondary_stats_html: # print("Warning: Secondary stats placeholder was not replaced.") # if "" in output_content and "" not in latest_interaction_display_html: # print("Warning: Latest interaction placeholder was not replaced.") # if "" in output_content and "" not in history_entries_html: # print("Warning: History entries placeholder was not replaced.") with open(DASHBOARD_OUTPUT_FILE, "w", encoding="utf-8") as f: f.write(output_content) print(f"Dashboard generated: {DASHBOARD_OUTPUT_FILE.resolve().as_uri()}") webbrowser.open(DASHBOARD_OUTPUT_FILE.resolve().as_uri()) if __name__ == "__main__": main()