import os import re import time import gradio as gr import markdown import pandas as pd import requests import services.db as db import xml.etree.ElementTree as ET from dotenv import load_dotenv from gradio_client import Client from gradio_htmlplus import HTMLPlus from gradio_bottombar import BottomBar from gradio_buttonplus import ButtonPlus from services import charts from services.table_renderer import generate_issues_html from services.chat_utils import stream_to_gradio from services.agent_chat import create_dashboard_agent from config.constants import AVAILABLE_MODELS_BY_PROVIDER load_dotenv() ACTIVE_SYNCS = set() DEFAULT_SORT = {"col": "updated_at", "asc": False} DEFAULT_REPO = "https://github.com/gradio-app/gradio" BLAXEL_ICON_URL = "https://huggingface.co/datasets/DEVAIEXP/assets/resolve/main/blaxel_logo.png" # Configuration MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "https://mcp-1st-birthday-gitrepo-inspector-mcp.hf.space/") AGENT_API_URL = os.getenv("AGENT_API_URL", "https://run.blaxel.ai/devaiexp/agents/agent") BLAXEL_API_KEY = os.getenv("BLAXEL_API_KEY") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") # LOGIC LAYER def _validate_api_keys(llm_api_key, request, type="Provider"): USE_SERVER_KEYS = os.getenv("USE_SERVER_KEYS", "false").lower() in ( "True", "true", "1", "yes", ) if not USE_SERVER_KEYS and request and request.headers.get("referer"): if not llm_api_key or not llm_api_key.strip(): raise gr.Error(f"⚠️ {type} API Key Required! Please enter your own API Key to use this tool in the demo UI.") def _get_custom_header(): html = """
πŸ•΅οΈβ€β™‚οΈ GitRepo Inspector
An Autonomous Ecosystem for GitHub Issue Management
""" return html.replace("{BLAXEL_ICON_URL}", BLAXEL_ICON_URL) def _format_logs_to_html(logs_df): """ Converts logs to HTML list with Icons and Client-side Timezone conversion. """ if logs_df.empty: return "
Waiting for agent activity...
" html_content = """
""" for _, row in logs_df.iterrows(): # ISO timestamp for JS to read (e.g., 2023-11-22T10:00:00Z) iso_time = row['created_at'].isoformat() etype = str(row['event_type']).lower() icon_char = "🧠" if 'thought' in etype else "πŸ”Œ" if 'tool' in etype else "❌" if 'error' in etype else "βœ…" if 'success' in etype else "ℹ️" issue_tag = f"[#{int(row['issue_number'])}] " if pd.notna(row['issue_number']) and row['issue_number'] != "None" else "" # Build HTML line html_content += f"""
icon --:--:-- {icon_char} {issue_tag}{row['message']}
""" html_content += "
" return html_content def parse_response_with_reasoning(text: str): try: root = ET.fromstring(f"{text.strip()}") response = root.findtext("response") or text.strip() reasoning = root.findtext("reasoning") or "" except: response = text.strip() reasoning = "" clean_response = re.sub(r"\[VIEW:#\d+\]", "", response).strip() if reasoning.strip(): accordion_html = f"""
RaciocΓ­nio do agente (clique para expandir)
{reasoning.strip().replace(chr(10), '
')}
""" return clean_response, accordion_html else: return clean_response, None def fetch_fresh_data_to_cache(log_limit=50, repo_url=None): """ Fetches all necessary data from DB. """ stats_df = db.fetch_dashboard_stats(repo_url) total_open = db.get_total_open_issues_count(repo_url) donut_fig = charts.create_verdict_donut(stats_df) funnel_fig = charts.create_efficiency_funnel(stats_df, total_open) logs_df = db.fetch_agent_logs(limit=log_limit) logs_text = _format_logs_to_html(logs_df) full_df = db.fetch_issues_dataframe("all", repo_url) timeline_fig = charts.create_timeline_chart(full_df) return donut_fig, funnel_fig, timeline_fig, full_df, logs_text def render_from_cache(df_cache, view_filter, sort_config, page=1): if df_cache is None or df_cache.empty: return generate_issues_html(pd.DataFrame()), "Page 1 / 1", 1 if "Action" in view_filter: df = df_cache[ (df_cache['status'] == 'pending_approval') & (df_cache['proposed_action'].notnull()) ] else: df = df_cache if sort_config and sort_config['col'] in df.columns: df = df.sort_values( by=sort_config['col'], ascending=sort_config['asc'], na_position='last' ) total_rows = len(df) total_pages = (total_rows // 50) + 1 if total_rows > 0 else 1 page = max(1, min(page, total_pages)) start = (page - 1) * 50 end = start + 50 df_page = df.iloc[start:end] html = generate_issues_html(df_page, sort_config.get('col'), sort_config.get('asc')) page_label = f"Page **{page}** of {total_pages} ({total_rows} items)" return html, page_label, page def refresh_dashboard_data(view_filter, sort_config=None, log_limit=50, repo_url=None): if sort_config is None: sort_config = DEFAULT_SORT donut_fig, timeline_fig, full_df, logs_text = fetch_fresh_data_to_cache(log_limit, repo_url) html_table, page_label, page = render_from_cache(full_df, view_filter, sort_config, 1) return donut_fig, timeline_fig, html_table, logs_text, full_df, page_label, page def load_issue_details(issue_number, repo_url): """ Loads details for an issue. Strategy: Check Agent Cache -> Fallback to DB. """ if not issue_number or issue_number == 0: return gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip() body = None action_json = None status = "pending_approval" thought_content = "" wrapped_html = "" body, action_json, status, thought_process = db.fetch_issue_details_by_id(issue_number, repo_url) raw_body = body if body else "No content." if "Analysis Report{final_html}" wrapped_html = f"""
{final_html}
""" def get_summary_text(st, act): if st == 'pending_approval': return f"### ⚠️ Action Required\n**Issue #{issue_number}** is marked as **{act.get('reason', 'resolved') if act else 'needs review'}**." elif st == 'executed': return f"### βœ… Action Executed\n**Issue #{issue_number}** was closed." else: return f"### ℹ️ Status: {st}" has_thought = thought_process is not None and len(thought_process.strip()) > 0 thought_content = thought_process if has_thought else "No thought process available." summary_text = get_summary_text(status, action_json) if not body: placeholder_html = f"""

Waiting for Analysis ⏳

This issue has not been processed by the Agent yet.

Click 'Analyze Now' to start manual analysis.

""" return ( placeholder_html, gr.update(value="", interactive=False, label="No Proposal"), "", gr.update(visible=False), gr.update(visible=True, interactive=True, value="▢️ Analyze Now"), summary_text, "", # Empty thought gr.update(visible=True, open=True), gr.update(visible=False, open=False) ) is_executed = (status == 'executed') is_pending = (status == 'pending_approval') comment_val = action_json.get('comment', '') if action_json else "" comment_update = gr.update( value=comment_val, interactive=is_pending, label="Executed Comment" if is_executed else "Proposed Comment" ) auth_vis = gr.update(visible=is_pending) reanalyze_vis = gr.update(visible=not is_executed, interactive=not is_executed, value="πŸ”„ Re-Analyze Issue") return wrapped_html, comment_update, "", auth_vis, reanalyze_vis, summary_text, thought_content, gr.update(visible=True, open=True), gr.update(visible=has_thought, open=False) def trigger_sync_action(repo_url, user_token): """ Calls the MCP sync tool for the selected repository. """ if not repo_url: error = "Please select or type a repository URL." gr.Info(f"⚠️ {error}") return error clean_url = repo_url.strip().rstrip("/") if not clean_url.startswith("https://github.com/"): error = "Invalid URL. Must start with https://github.com/" gr.Info(f"⚠️ {error}") return error if clean_url in ACTIVE_SYNCS: error = "Sync already in progress for {clean_url}. Please wait..." gr.Info(f"⚠️ {error}") return error if not user_token: error = "GitHub Token Required!" gr.Info(f"⚠️ {error}") return error ACTIVE_SYNCS.add(clean_url) try: client = Client(MCP_SERVER_URL) job = client.submit( repo_url, user_token, api_name="/sync_repository" ) for update in job: yield f"{update}" except Exception as e: yield f"❌ Sync Failed: {str(e)}" finally: if clean_url in ACTIVE_SYNCS: ACTIVE_SYNCS.remove(clean_url) def trigger_manual_reanalysis(issue_number, repo_url, provider, model, github_token, api_key, request: gr.Request = None): """ Calls the Blaxel Agent to force a re-analysis. """ if not issue_number: error = "Select an issue first." gr.Info(f"⚠️ {error}") return error _validate_api_keys(api_key, request) if not AGENT_API_URL: error = "Agent URL not configured." gr.Info(f"⚠️ {error}") return error payload = { "repo_url": repo_url, "provider": provider, "model": model, "specific_issue": int(issue_number), "github_token": github_token if github_token else None } gr.Info(f"⏳ Starting analysis for #{issue_number}. Please wait...") try: headers = {"Authorization": f"Bearer {BLAXEL_API_KEY}", "Content-Type": "application/json"} with requests.post(AGENT_API_URL, json=payload, headers=headers, stream=True) as resp: for _ in resp.iter_lines(): pass return f"βœ… Analysis completed for #{issue_number}. Check the Trace Log tab for details." except Exception as e: return f"❌ Agent Trigger Failed: {e}" def execute_approval_workflow(issue_number, repo_url, user_token, final_comment): if not user_token: error = "GitHub Token Required! Check Sidebar." gr.Info(f"⚠️ {error}") return error try: proposal = db.get_proposed_action_payload(issue_number, repo_url) # Flexible Logic: # If proposal exists, use its 'close' flag. # If NO proposal (user manual action), assume they want to close. should_close = True if proposal: should_close = proposal.get('action') == 'close' # If user didn't type comment, use proposal or default if not final_comment: if proposal: final_comment = proposal.get('comment', "Closing via GitRepo Inspector.") else: final_comment = "Closing via GitRepo Inspector (Manual Action)." client = Client(MCP_SERVER_URL) result = client.predict( repo_url, int(issue_number), final_comment, should_close, user_token, api_name="/reply_and_close_issue" ) db.update_issue_status(issue_number, repo_url, "executed", final_comment) return f"βœ… Success: {result}" except Exception as e: # If it's a gr.Error, let it raise to show popup if isinstance(e, gr.Error): raise e # If other error, format it return f"❌ Error: {str(e)}" def generate_priority_report(repo_url, provider, model, api_key, request: gr.Request = None): """ Generates priority report using Sidebar configs. Handles tuple return (html, thought) from MCP. """ _validate_api_keys(api_key, request) gr.Info(f"🧠 Generating Strategy Report for {repo_url}... Please wait.") try: client = Client(MCP_SERVER_URL) job = client.submit( repo_url, provider, model, api_key, api_name="/prioritize_open_issues" ) for tuple_result in job: if isinstance(tuple_result, (list, tuple)) and len(tuple_result) > 0: html_content = tuple_result[0] yield html_content else: yield tuple_result except Exception as e: return f"❌ Error: {str(e)}" def get_repo_choices(): repos = db.fetch_distinct_repos() if DEFAULT_REPO not in repos: repos.insert(0, DEFAULT_REPO) return repos # UI LAYOUT css_code = "" try: with open("./style.css", "r", encoding="utf-8") as f: css_code += f.read() + "\n" except FileNotFoundError: pass # JS APP_HEAD = """ """ theme = gr.themes.Default( primary_hue='blue', secondary_hue='teal', neutral_hue='neutral' ).set( body_background_fill='*neutral_100', body_background_fill_dark='*neutral_900', body_text_color='*neutral_700', body_text_color_dark='*neutral_200', body_text_weight='400', link_text_color='*primary_500', link_text_color_dark='*primary_400', code_background_fill='*neutral_100', code_background_fill_dark='*neutral_800', shadow_drop='0 1px 3px rgba(0,0,0,0.1)', shadow_inset='inset 0 2px 4px rgba(0,0,0,0.05)', block_background_fill='*neutral_50', block_background_fill_dark='*neutral_700', block_border_color='*neutral_200', block_border_color_dark='*neutral_600', block_border_width='1px', block_border_width_dark='1px', block_label_background_fill='*primary_50', block_label_background_fill_dark='*primary_600', block_label_text_color='*primary_600', block_label_text_color_dark='*primary_50', panel_background_fill='white', panel_background_fill_dark='*neutral_800', panel_border_color='*neutral_200', panel_border_color_dark='*neutral_700', panel_border_width='1px', panel_border_width_dark='1px', input_background_fill='white', input_background_fill_dark='*neutral_800', input_border_color='*neutral_300', input_border_color_dark='*neutral_700', slider_color='*primary_500', slider_color_dark='*primary_400', button_primary_background_fill='*primary_600', button_primary_background_fill_dark='*primary_500', button_primary_background_fill_hover='*primary_700', button_primary_background_fill_hover_dark='*primary_400', button_primary_border_color='transparent', button_primary_border_color_dark='transparent', button_primary_text_color='white', button_primary_text_color_dark='white', button_secondary_background_fill='*neutral_200', button_secondary_background_fill_dark='*neutral_600', button_secondary_background_fill_hover='*neutral_300', button_secondary_background_fill_hover_dark='*neutral_500', button_secondary_border_color='transparent', button_secondary_border_color_dark='transparent', button_secondary_text_color='*neutral_700', button_secondary_text_color_dark='*neutral_200' ) with gr.Blocks(title="GitRepo Inspector", theme=theme, css=css_code, head=APP_HEAD) as app: # STATES current_sort = gr.State(DEFAULT_SORT) issues_cache = gr.State(pd.DataFrame()) current_page = gr.State(1) sel_issue_state = gr.State() sel_repo_state = gr.State() agent_state = gr.State(value=None) with gr.Sidebar(position="left", open=True, label="Settings"): gr.Markdown("### βš™οΈ Configuration") # Repository Selection repo_choices = get_repo_choices() global_repo_url = gr.Dropdown( elem_classes="custom-dropdown", choices=repo_choices, value=DEFAULT_REPO, label="Repository", allow_custom_value=True, interactive=True, info="Select existing or type new URL" ) with gr.Row(): sync_repo_btn = ButtonPlus("πŸ”„ Sync This Repo", help="Update Repository Issues Cache. This may take a while.") sync_status = gr.Markdown(visible=True, value="Ready to sync.") global_github_token = gr.Textbox( label="GitHub Personal Token", type="password", placeholder="ghp_...", info="Required for 'Approve' and 'Prioritize'" ) gr.Markdown("---") gr.Markdown("### 🧠 AI Model Configuration") gr.Markdown("(For Manual Actions)") default_provider = "gemini" with gr.Group(): global_provider = gr.Dropdown( choices=list(AVAILABLE_MODELS_BY_PROVIDER.keys()), value=default_provider, label="LLM Provider" ) # Use default provider list global_model = gr.Dropdown( choices=AVAILABLE_MODELS_BY_PROVIDER[default_provider], value=AVAILABLE_MODELS_BY_PROVIDER[default_provider][0], label="Model Name", allow_custom_value=True, interactive=True ) with gr.Group(): global_llm_key = gr.Textbox( label="Provider API Key", type="password", placeholder="your API Key...", info="Required for approval and reanalysis actions" ) global_gemini_key = gr.Textbox( label="Gemini Assistant API Key", type="password", placeholder="your API Key...", info="Gemini API Key required for AI Assistant Chat" ) gr.Markdown("---") log_limit_slider = gr.Slider( minimum=10, maximum=200, value=50, step=10, label="Log Lines to Fetch" ) gr.HTML(_get_custom_header()) # Control Bar with gr.Row(): with gr.Column(scale=1): pass with gr.Column(scale=0, min_width=150): refresh_global = gr.Button("πŸ”„ Refresh Data", variant="secondary") with gr.Sidebar(position="right", open=False, width=400): with gr.Column(scale=1, elem_classes="column-container", min_width=400): gr.Markdown("### πŸ€– AI Assistant") with gr.Column(scale=2): chatbot = gr.Chatbot( elem_classes="chatbot-container", type="messages", height=500, show_label=False, show_copy_button=True, avatar_images=( "https://huggingface.co/datasets/DEVAIEXP/assets/resolve/main/user.png", "https://huggingface.co/datasets/DEVAIEXP/assets/resolve/main/gemini-icon.png" ) ) with gr.Row(): msg = gr.Textbox(label="Message", max_lines=4, scale=4, placeholder="Ask: 'What's up with issue #123?'") send_btn = gr.Button("Send", variant="primary", scale=1) with gr.Row(): clear_chat_btn = gr.Button("🧹 Clear History") with gr.Row(max_height=300): gr.Examples(elem_id="chat-examples", examples=[ ["Whats up with issue 12021?"], ["Which repository are we analyzing right now?"], ["List 3 open issues about 'chatbot'."], ["Are there any issues marked as 'duplicate' by the AI?"], ["Show me issues created by user 'abidlabs' (or any user you know exists)."], ["Show me the full report for issue #12314"], ["Please analyze issue #12264 again right now"], ["Check issue #12044 using the 'openai' provider and model 'gpt-4o'"], ["Analyze issue #12432 using 'gpt-oss-120b'"], ["Is issue #11910 a duplicate? Check now."], ["Find the most recent open issue about 'bug' and tell me if it's already resolved."], ["Do we have any duplicates related to 'audio'? If yes, which is the original issue?"], ["I'm seeing an error '422 Unprocessable Entity'. Is this a known issue in the repo?"], ["What is the latest version of Gradio library?"], ["This issue mentions 'Pydantic v2'. What is the migration guide for that?"], ], inputs=[msg], label="Message Examples", ) chat_trigger_issue = gr.Number(visible=False) def clear_chat(): return [] clear_chat_btn.click( fn=clear_chat, inputs=None, outputs=[chatbot] ) def init_agent(gemini_api_key): return create_dashboard_agent(gemini_api_key) def interact(agent, prompt, history, current_repo, provider, model, token, gemini_api_key, request: gr.Request=None): agent_state = agent if agent_state is None: _validate_api_keys(gemini_api_key, request, "Gemini Assistant") gr.Info("⚑ Please wait, initializing the agent for the first time...") agent_state = init_agent(gemini_api_key) if agent_state is None: history.append(gr.ChatMessage(role="user", content=prompt, metadata={"status": "done"})) history.append(gr.ChatMessage( role="assistant", content="❌ Failed to initialize agent", metadata={"status": "done"} )) yield history, "", gr.skip(), agent_state return time.sleep(2) context_prompt = f""" User is looking at repository: '{current_repo}' Selected analysis provider: '{provider}' Selected model: '{model}' GitHub Token provided: {'Yes' if token else 'No'} User Query: {prompt} """ history.append(gr.ChatMessage(role="user", content=prompt)) yield history, "", gr.skip(), agent_state full_response = "" detected_issue = gr.skip(), agent_state if agent_state: try: for chunk in stream_to_gradio(agent_state, context_prompt): if isinstance(chunk, gr.ChatMessage): history.append(chunk) elif isinstance(chunk, str): if history and history[-1].role == "assistant": history[-1].content += chunk else: history.append(gr.ChatMessage(role="assistant", content=chunk)) content = chunk.content if isinstance(chunk, gr.ChatMessage) else chunk full_response += str(content) yield history, "", gr.skip(), agent_state # Check Trigger #triggers = re.findall(r"\[VIEW:#(\d+)\]", full_response) match = re.search(r"\[VIEW:#(\d+)\]\s*$", full_response, re.MULTILINE) detected_issue = int(match.group(1)) if match else None if match: ## Get the last mentioned (or first, your choice) # Usually the last is the conclusion focus #detected_issue = int(match[-1]) # Remove ALL [VIEW:#...] tags from final message last_msg = history[-1] if isinstance(last_msg, gr.ChatMessage): clean_content = re.sub(r"\[VIEW:#\d+\]", "", last_msg.content).strip() last_msg.content = clean_content elif isinstance(last_msg, dict): last_msg['content'] = re.sub(r"\[VIEW:#\d+\]", "", last_msg['content']).strip() yield history, "", detected_issue, agent_state except Exception as e: history.append(gr.ChatMessage(role="assistant", content=f"Error: {e}")) yield history, "", gr.skip(), agent_state else: history.append(gr.ChatMessage(role="assistant", content="Agent failed to initialize.")) yield history, "", gr.skip(), agent_state submit_event = gr.on( triggers=[send_btn.click, msg.submit], fn=interact, inputs=[ agent_state, msg, chatbot, global_repo_url, global_provider, global_model, global_github_token, global_gemini_key ], outputs=[chatbot, msg, chat_trigger_issue, agent_state], show_progress="hidden" ) with gr.Row(): with gr.Column(scale=3, elem_classes="column-container"): # MAIN CONTENT with gr.Tabs(): with gr.TabItem("πŸ“‹ Issues Overview"): with gr.Row(): # Left: Table with gr.Column(): view_filter = gr.Radio( ["Action Required", "All Issues"], value="Action Required", label="View Filter", show_label=False, container=False ) html_table_output = HTMLPlus( label="Issue List", selectable_elements=["th", "tr"] ) with gr.Row(elem_id="pagination-row"): btn_prev = gr.Button("β—€", elem_classes="pagination-btn", size="sm") page_display = gr.Markdown("Page 1 of 1", elem_id="page_label") btn_next = gr.Button("β–Ά", elem_classes="pagination-btn", size="sm") # Details & Action with gr.Group(visible=True): with gr.Accordion("πŸ“„ Issue Analysis Detail", open=False, visible=False) as issue_analysis_accordion: with gr.Row(): with gr.Column(scale=3): detail_view = gr.HTML(label="Full Report") with gr.Column(scale=1, elem_classes="action-console-col"): with gr.Group() as action_group: gr.Markdown("#### ⚑ Action Console") action_summary = gr.Markdown("Select an issue.") action_comment_input = gr.Textbox(label="Proposed Comment", lines=3, interactive=True) reanalyze_btn = gr.Button("πŸ”„ Re-Analyze Issue") with gr.Column(visible=False) as auth_box: approve_btn = gr.Button("βœ… Approve Action", variant="primary") exec_status = gr.Markdown() with gr.Accordion("🧠 Agent Thought Process", open=False, visible=False) as thought_accordion: thought_view = gr.Markdown("Select an issue to see the reasoning.") gr.Markdown("---") # Charts gr.Markdown("### πŸ“Š Analytics Overview") with gr.Row(): with gr.Column(): stats_plot = gr.Plot(label="Distribution", show_label=False) with gr.Column(): funnel_plot = gr.Plot(label="Efficiency", show_label=False) with gr.Column(): timeline_plot = gr.Plot(label="Timeline", show_label=False) with gr.TabItem("πŸ₯‡ Prioritize"): gr.Markdown("### Strategic Backlog Prioritization") with gr.Row(): prio_btn = gr.Button("Generate Strategy Report 🧠", variant="primary") print_btn = gr.Button("πŸ–¨οΈ Print Report", variant="secondary") with gr.Group(): prio_out = gr.HTML(label="AI Strategy Report", elem_id="prio-report") # BOTTOM BAR with BottomBar(label="πŸ€– AI Assistant", bring_to_front=False, height=320, open=True, rounded_borders=True): gr.Markdown("### πŸ“Š Workflow Agent Activity Logs", elem_id="logs-header") with gr.Row(): auto_refresh_timer = gr.Timer(value=30, active=True) with gr.Column(scale=1): refresh_interval = gr.Slider( info="Auto-refresh interval", minimum=30, maximum=300, value=30, step=30, label="Auto-Refresh (seconds)", interactive=True, ) with gr.Column(scale=6): activity_log = gr.HTML(label="Trace Log", elem_id="trace-log") # EVENTS trigger_outputs = [ detail_view, action_comment_input, exec_status, auth_box, reanalyze_btn, action_summary, thought_view, issue_analysis_accordion, thought_accordion, chat_trigger_issue ] def handle_chat_view_trigger(issue_number, repo_url): if not issue_number: return tuple(gr.skip() for _ in range(len(trigger_outputs))) # Small pause to ensure DB write propagated time.sleep(3) # Call function to load data, which will now find the record details = load_issue_details(issue_number, repo_url) return details + (None,) chat_trigger_issue.change( fn=handle_chat_view_trigger, inputs=[chat_trigger_issue, global_repo_url], outputs=trigger_outputs ) # Refresh Logic def hard_refresh(filter_val, sort_val, log_lim, repo_url=None): repo = repo_url if repo_url else "https://github.com/gradio-app/gradio" donut_fig, funnel_fig, timeline_fig, full_df, logs = fetch_fresh_data_to_cache(log_lim, repo) html, label, page = render_from_cache(full_df, filter_val, sort_val, 1) return ( donut_fig, # stats_plot timeline_fig, # timeline_plot funnel_fig, # funnel_plot html, # html_table_output logs, # activity_log full_df, # issues_cache label, # page_display page, # current_page "", # detail_view "Select an issue.", # action_summary gr.update(visible=False), # auth_box "", # exec_status gr.update(visible=False), # thought_accordion gr.update(visible=False, open=False), # issue_analysis_accordion "" # thought_view ) common_outputs = [ stats_plot, funnel_plot, timeline_plot, html_table_output, activity_log, issues_cache, page_display, current_page, detail_view, action_summary, auth_box, exec_status, thought_accordion, issue_analysis_accordion, thought_view ] # Added log_limit_slider to inputs # app.load( # init_agent, # outputs=[agent_state] # ) app.load( fn=hard_refresh, inputs=[view_filter, current_sort, log_limit_slider, global_repo_url], outputs=common_outputs, show_progress_on=[ html_table_output, stats_plot, funnel_plot, timeline_plot, ] ).success( fn=None, js="() => window.convert_timestamps()" ) refresh_global.click( fn=hard_refresh, inputs=[view_filter, current_sort, log_limit_slider, global_repo_url], outputs=common_outputs, show_progress_on=[ html_table_output, stats_plot, funnel_plot, timeline_plot, ] ).then( fn=None, js="() => window.convert_timestamps()" ) def quick_log_refresh(log_lim): df = db.fetch_agent_logs(limit=log_lim) return _format_logs_to_html(df) auto_refresh_timer.tick( fn=quick_log_refresh, inputs=[log_limit_slider], outputs=[activity_log] ).then( fn=None, js="() => window.convert_timestamps()" ) # Update timer when slider changes refresh_interval.change( fn=lambda x: gr.Timer(value=x), inputs=[refresh_interval], outputs=[auto_refresh_timer] ) def update_dashboard_models(provider): models = AVAILABLE_MODELS_BY_PROVIDER.get(provider, []) # Select first from list by default, or empty if none new_val = models[0] if models else None return gr.update(choices=models, value=new_val) global_provider.change( fn=update_dashboard_models, inputs=[global_provider], outputs=[global_model] ) # Soft Updates def soft_update(df, filter_val, sort_val): html, label, page = render_from_cache(df, filter_val, sort_val, 1) return html, label, page view_filter.change(fn=soft_update, inputs=[issues_cache, view_filter, current_sort], outputs=[html_table_output, page_display, current_page]) def change_page(direction, current, df, filter_val, sort_val): new_page = current + direction html, label, final_page = render_from_cache(df, filter_val, sort_val, new_page) return html, label, final_page btn_prev.click(fn=lambda p, df, f, s: change_page(-1, p, df, f, s), inputs=[current_page, issues_cache, view_filter, current_sort], outputs=[html_table_output, page_display, current_page]) btn_next.click(fn=lambda p, df, f, s: change_page(1, p, df, f, s), inputs=[current_page, issues_cache, view_filter, current_sort], outputs=[html_table_output, page_display, current_page]) # Selection def handle_table_interaction(evt: gr.SelectData, df_cache, view_filter, sort_data): data = evt.value if evt.index == "th": clicked_col = data.get('sortCol') or data.get('sort-col') if not clicked_col: return gr.skip() new_asc = not sort_data['asc'] if sort_data['col'] == clicked_col else False new_sort = {"col": clicked_col, "asc": new_asc} html, _, _ = render_from_cache(df_cache, view_filter, new_sort, page=1) return ( new_sort, html, gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), ) elif evt.index == "tr": issue_num = data.get('issueNumber') or data.get('issue-number') repo_url = data.get('repoUrl') or data.get('repo-url') if not issue_num: return gr.skip() report, action_text, status, auth_vis, reanalyze_vis, summary_text, thought_text, issue_accordion, thought_accordion = load_issue_details(issue_num, repo_url) return ( gr.skip(), gr.skip(), report, action_text, auth_vis, issue_num, repo_url, "", reanalyze_vis, thought_accordion, issue_accordion, summary_text, thought_text ) return gr.skip() html_table_output.select( fn=handle_table_interaction, inputs=[issues_cache, view_filter, current_sort], outputs=[ current_sort, html_table_output, detail_view, action_comment_input, auth_box, sel_issue_state, sel_repo_state, exec_status, reanalyze_btn, thought_accordion, issue_analysis_accordion, action_summary, thought_view ], show_progress_on=[issue_analysis_accordion] ) # Actions approve_btn.click( fn=execute_approval_workflow, inputs=[sel_issue_state, sel_repo_state, global_github_token, action_comment_input], outputs=[exec_status] ).success( fn=hard_refresh, inputs=[view_filter, current_sort, log_limit_slider, global_repo_url], outputs=common_outputs, show_progress_on=[ html_table_output, stats_plot, funnel_plot, timeline_plot, ] ) prio_btn.click( fn=generate_priority_report, inputs=[global_repo_url, global_provider, global_model, global_llm_key], outputs=[prio_out], show_progress="hidden" ) print_btn.click(fn=None, js="() => window.print_report()") global_repo_url.change( fn=hard_refresh, inputs=[view_filter, current_sort, log_limit_slider, global_repo_url], outputs=common_outputs, show_progress_on=[ html_table_output, stats_plot, funnel_plot, timeline_plot, ] ) # Sync Button Logic sync_repo_btn.click( fn=trigger_sync_action, inputs=[global_repo_url, global_github_token], outputs=[sync_status], show_progress="hidden", concurrency_limit=10 ).success( fn=hard_refresh, inputs=[view_filter, current_sort, log_limit_slider, global_repo_url], outputs=common_outputs, show_progress_on=[ html_table_output, stats_plot, funnel_plot, timeline_plot, ] ) reanalyze_btn.click( fn=trigger_manual_reanalysis, inputs=[ sel_issue_state, sel_repo_state, global_provider, global_model, global_github_token, global_llm_key ], outputs=[exec_status] ).success( fn=hard_refresh, inputs=[view_filter, current_sort, log_limit_slider, global_repo_url], outputs=common_outputs, show_progress_on=[ html_table_output, stats_plot, funnel_plot, timeline_plot, ] ) if __name__ == "__main__": app.launch()