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 = """
"""
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 = """
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()