.*?)?\.md$')
+
+ # Get the root directory
+ root_dir = Path(__file__).parent.parent
+
+ # Find all report files in the root directory
+ migrated_count = 0
+ for file_path in root_dir.glob('report_*.md'):
+ if not file_path.is_file():
+ continue
+
+ # Extract information from the filename
+ match = report_pattern.match(file_path.name)
+ if not match:
+ continue
+
+ date_str = match.group('date')
+ time_str = match.group('time')
+ model = match.group('model') or 'unknown'
+
+ # Format date for directory structure (YYYY-MM-DD)
+ try:
+ year = date_str[:4]
+ month = date_str[4:6]
+ day = date_str[6:8]
+ formatted_date = f"{year}-{month}-{day}"
+
+ # Create timestamp for metadata
+ timestamp = f"{year}-{month}-{day} {time_str[:2]}:{time_str[2:4]}:{time_str[4:6]}"
+ creation_date = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").isoformat()
+ except ValueError:
+ # If date parsing fails, use current date
+ formatted_date = datetime.now().strftime("%Y-%m-%d")
+ creation_date = datetime.now().isoformat()
+
+ # Create directory for the date if it doesn't exist
+ date_dir = self.reports_dir / formatted_date
+ date_dir.mkdir(exist_ok=True)
+
+ # Generate a unique report ID
+ report_id = f"{date_str}_{time_str}"
+
+ # Copy the file to the new location
+ new_file_path = date_dir / file_path.name
+ shutil.copy2(file_path, new_file_path)
+
+ # Read the report content to extract query if possible
+ query = ""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read(1000) # Read just the beginning to find the query
+ # Try to extract query from title or first few lines
+ title_match = re.search(r'#\s*(.+?)\n', content)
+ if title_match:
+ query = title_match.group(1).strip()
+ else:
+ # Just use the first line as query
+ query = content.split('\n')[0].strip()
+ except Exception as e:
+ print(f"Error reading file {file_path}: {e}")
+
+ # Create metadata for the report
+ file_size = os.path.getsize(file_path)
+ metadata = {
+ "id": report_id,
+ "query": query,
+ "model": model,
+ "detail_level": "unknown", # We don't know the detail level from the filename
+ "timestamp": timestamp,
+ "creation_date": creation_date,
+ "file_path": str(new_file_path),
+ "file_size": file_size
+ }
+
+ # Update the metadata file
+ self._update_report_metadata(report_id, metadata)
+ migrated_count += 1
+
+ return f"Migrated {migrated_count} existing reports to the new directory structure."
def create_interface(self):
"""
@@ -624,53 +1077,9 @@ class GradioInterface:
"""
)
+ # Create tabs for different sections
with gr.Tabs() as tabs:
- with gr.TabItem("Search"):
- with gr.Row():
- with gr.Column(scale=4):
- search_query_input = gr.Textbox(
- label="Research Query",
- placeholder="Enter your research question here...",
- lines=3
- )
- with gr.Column(scale=1):
- search_num_results = gr.Slider(
- minimum=5,
- maximum=50,
- value=20,
- step=5,
- label="Results Per Engine"
- )
- search_use_reranker = gr.Checkbox(
- label="Use Semantic Reranker",
- value=True,
- info="Uses Jina AI's reranker for more relevant results"
- )
- search_button = gr.Button("Search", variant="primary")
-
- gr.Examples(
- examples=[
- ["What are the latest advancements in quantum computing?"],
- ["Compare transformer and RNN architectures for NLP tasks"],
- ["Explain the environmental impact of electric vehicles"],
- ["What recent actions has Trump taken regarding tariffs?"],
- ["What are the recent papers on large language model alignment?"],
- ["What are the main research findings on climate change adaptation strategies in agriculture?"]
- ],
- inputs=search_query_input
- )
-
- with gr.Row():
- with gr.Column():
- search_results_output = gr.Markdown(label="Results")
-
- with gr.Row():
- with gr.Column():
- search_file_output = gr.Textbox(
- label="Results saved to file",
- interactive=False
- )
-
+ # Report Generation Tab
with gr.TabItem("Generate Report"):
with gr.Row():
with gr.Column(scale=4):
@@ -779,19 +1188,163 @@ class GradioInterface:
gr.Markdown(f"### Detail Levels\n{detail_levels_info}")
gr.Markdown(f"### Query Types\n{query_types_info}")
+
+ # Report Management Tab
+ with gr.TabItem("Manage Reports"):
+ with gr.Row():
+ gr.Markdown("## Report Management")
+
+ with gr.Row():
+ gr.Markdown("Select reports to download or delete. You can also filter and sort the reports.")
+
+ # Create a state to store the current reports
+ reports_state = gr.State([])
+
+ # Only include one view of the reports with a clean selection interface
+ with gr.Row():
+ with gr.Column():
+ gr.Markdown("### Reports")
+ # Get the reports data
+ reports_data = self._get_reports_for_display()
+ # This hidden table is just used to store the data
+ reports_table = gr.Dataframe(
+ headers=["ID", "Query", "Model", "Detail Level", "Created", "Size", "Filename"],
+ datatype=["str", "str", "str", "str", "str", "str", "str"],
+ value=reports_data,
+ visible=False, # Hide this table
+ interactive=False
+ )
+
+ # Get the choices for the checkbox group
+ initial_choices = self._get_report_choices(reports_data)
+ print(f"Initial choices generated: {len(initial_choices)}")
+ if not initial_choices:
+ initial_choices = ["No reports available"]
+
+ # Use a cleaner component approach with better styling
+ gr.Markdown("##### Select reports below for download or deletion")
+
+ # Create a completely custom HTML solution for maximum control
+ # Prepare the HTML for the checkboxes
+ html_choices = []
+ for i, choice in enumerate(initial_choices):
+ html_choices.append(f'')
+ html_choices.append(f'')
+ html_choices.append('
')
+
+ # Create the HTML string with all the checkbox markup and JavaScript functionality
+ html_content = f"""
+
+
+
+
+
+
+ {''.join(html_choices)}
+
+
+
+
+ """
+
+ # Create HTML component with our custom checkbox implementation
+ custom_html = gr.HTML(html_content)
+
+ # Create a hidden Textbox to store the selected values as JSON
+ reports_checkboxes = gr.Textbox(
+ value="[]", # Empty array as initial value
+ visible=False, # Hide this
+ elem_id="reports-hidden-value"
+ )
+
+ gr.Markdown("*Check the boxes next to the reports you want to manage*")
+
+ # Buttons for report management
+ with gr.Row():
+ with gr.Column(scale=1):
+ refresh_button = gr.Button("Refresh List")
+ with gr.Column(scale=1):
+ download_button = gr.Button("Download Selected")
+ with gr.Column(scale=1):
+ delete_button = gr.Button("Delete Selected", variant="stop")
+ with gr.Column(scale=2):
+ cleanup_days = gr.Slider(
+ minimum=0,
+ maximum=90,
+ value=30,
+ step=1,
+ label="Delete Reports Older Than (Days)",
+ info="Set to 0 to disable automatic cleanup"
+ )
+ cleanup_button = gr.Button("Clean Up Old Reports")
+
+ # File download component
+ with gr.Row():
+ file_output = gr.File(
+ label="Downloaded Reports",
+ file_count="multiple",
+ type="filepath",
+ interactive=False
+ )
+
+ # Status message
+ with gr.Row():
+ status_message = gr.Markdown("")
+
+ # Migration button for existing reports
+ with gr.Row():
+ with gr.Column():
+ gr.Markdown("### Migrate Existing Reports")
+ gr.Markdown("Use this button to migrate existing reports from the root directory to the new reports directory structure.")
+ migrate_button = gr.Button("Migrate Existing Reports", variant="primary")
# Set up event handlers
- search_button.click(
- fn=self.process_query,
- inputs=[search_query_input, search_num_results, search_use_reranker],
- outputs=[search_results_output, search_file_output]
- )
-
- # Connect the progress callback to the report button
- # Progress display is now handled entirely by Gradio's built-in progress tracking
-
# Update the progress tracking in the generate_report method
- async def generate_report_with_progress(query, detail_level, query_type, model_name, rerank, token_budget, initial_results, final_results):
+ async def generate_report_with_progress(query, detail_level, query_type, model_name, process_thinking, initial_results, final_results):
# Set up progress tracking
progress_data = gr.Progress(track_tqdm=True)
@@ -799,17 +1352,255 @@ class GradioInterface:
print(f"Model selected from UI dropdown: {model_name}")
# Call the original generate_report method
- result = await self.generate_report(query, detail_level, query_type, model_name, rerank, token_budget, initial_results, final_results)
+ result = await self.generate_report(
+ query,
+ detail_level,
+ query_type,
+ model_name,
+ None, # results_file is now None since we removed the search tab
+ process_thinking,
+ initial_results,
+ final_results
+ )
return result
report_button.click(
- fn=lambda q, d, t, m, r, p, i, f: asyncio.run(generate_report_with_progress(q, d, t, m, r, p, i, f)),
+ fn=lambda q, d, t, m, p, i, f: asyncio.run(generate_report_with_progress(q, d, t, m, p, i, f)),
inputs=[report_query_input, report_detail_level, report_query_type, report_custom_model,
- search_file_output, report_process_thinking, initial_results_slider, final_results_slider],
+ report_process_thinking, initial_results_slider, final_results_slider],
outputs=[report_output, report_file_output]
)
+ # Report Management Tab Event Handlers
+ def refresh_reports():
+ reports_data = self._get_reports_for_display()
+ choices = self._get_report_choices(reports_data)
+ return reports_data, choices
+
+ refresh_button.click(
+ fn=refresh_reports,
+ inputs=[],
+ outputs=[reports_table, reports_checkboxes]
+ )
+
+ # Add wrapper to parse JSON and handle download
+ def download_with_logging(selected_json):
+ try:
+ # Parse the JSON string from the hidden textbox
+ import json
+ print(f"Raw selected_json: '{selected_json}'")
+
+ # Make sure we have valid JSON before parsing
+ if not selected_json or selected_json.strip() == "":
+ selected = []
+ else:
+ # Handle potential edge cases by cleaning up the input
+ cleaned_json = selected_json.strip()
+ if not (cleaned_json.startswith('[') and cleaned_json.endswith(']')):
+ cleaned_json = f"[{cleaned_json}]"
+
+ selected = json.loads(cleaned_json)
+
+ print(f"Download button clicked with selections: {selected}")
+ files = self._download_selected_reports(selected)
+ print(f"Files prepared for download: {len(files)}")
+ return files
+ except Exception as e:
+ print(f"Error processing selections for download: {e}")
+ import traceback
+ traceback.print_exc()
+ return []
+
+ # Connect download button directly to our handler
+ download_button.click(
+ fn=download_with_logging,
+ inputs=reports_checkboxes, # Now contains JSON string of selections
+ outputs=file_output
+ )
+
+ # No need for toggle functionality as it's handled by JavaScript in the HTML component
+
+ # Add logging wrapper for delete function
+ def delete_with_logging(selected):
+ print(f"Delete button clicked with selections: {selected}")
+ updated_table, updated_choices, message = self._delete_selected_reports(selected)
+ print(f"After deletion: {len(updated_table)} reports, {len(updated_choices)} choices")
+ return updated_table, updated_choices, message
+
+ # Update delete handler to parse JSON with improved error handling
+ def delete_with_reset(selected_json):
+ try:
+ # Parse the JSON string from the hidden textbox
+ import json
+ print(f"Raw selected_json for delete: '{selected_json}'")
+
+ # Make sure we have valid JSON before parsing
+ if not selected_json or selected_json.strip() == "":
+ selected = []
+ else:
+ # Handle potential edge cases by cleaning up the input
+ cleaned_json = selected_json.strip()
+ if not (cleaned_json.startswith('[') and cleaned_json.endswith(']')):
+ cleaned_json = f"[{cleaned_json}]"
+
+ selected = json.loads(cleaned_json)
+
+ print(f"Delete button clicked with selections: {selected}")
+ updated_table, updated_choices, message = self._delete_selected_reports(selected)
+ print(f"After deletion: {len(updated_table)} reports, {len(updated_choices)} choices")
+
+ # Generate new HTML after deletion
+ html_choices = []
+ for i, choice in enumerate(updated_choices):
+ html_choices.append(f'')
+ html_choices.append(f'')
+ html_choices.append('
')
+
+ html_content = f"""
+
+
+
+
+
+
+ {''.join(html_choices)}
+
+
+
+
+ """
+
+ # Reset hidden field
+ return updated_table, html_content, "[]", message
+ except Exception as e:
+ print(f"Error processing selections: {e}")
+ return reports_table, custom_html.value, "[]", f"Error: {str(e)}"
+
+ delete_button.click(
+ fn=delete_with_reset,
+ inputs=reports_checkboxes,
+ outputs=[reports_table, custom_html, reports_checkboxes, status_message]
+ ).then(
+ fn=lambda msg: f"{msg} Selected reports deleted successfully.",
+ inputs=[status_message],
+ outputs=[status_message]
+ )
+
+ def cleanup_with_refresh(days):
+ updated_table = self._cleanup_old_reports(days)
+ choices = self._get_report_choices(updated_table)
+ message = f"Reports older than {days} days have been deleted."
+ print(message)
+ return updated_table, choices, message
+
+ # Note: We need to make sure this runs properly and updates both the table and checkboxes
+ # The built-in Gradio progress tracking (gr.Progress) is used instead
+ # This is passed to the generate_report method and handles progress updates
+
+ cleanup_button.click(
+ fn=cleanup_with_refresh,
+ inputs=cleanup_days,
+ outputs=[reports_table, reports_checkboxes, status_message]
+ ).then(
+ # Add a then function to ensure the UI updates properly
+ fn=lambda: "Report list has been refreshed.",
+ inputs=[],
+ outputs=[status_message]
+ )
+
+ # Migration button event handler
+ def migrate_and_refresh():
+ print("Starting migration of existing reports...")
+ status = self.migrate_existing_reports()
+ print("Migration completed, refreshing display...")
+ reports_data = self._get_reports_for_display()
+ print(f"Got {len(reports_data)} reports for display")
+ choices = self._get_report_choices(reports_data)
+ print(f"Generated {len(choices)} choices for selection")
+ return status, reports_data, choices
+
+ migrate_button.click(
+ fn=migrate_and_refresh,
+ inputs=[],
+ outputs=[status_message, reports_table, reports_checkboxes]
+ ).then(
+ # Add a confirmation message after migration completes
+ fn=lambda msg: f"{msg} Report list has been refreshed.",
+ inputs=[status_message],
+ outputs=[status_message]
+ )
+
+ # Initialize the checkboxes when the table is first loaded
+ # reports_table.change(
+ # fn=lambda table: self._get_report_choices(table),
+ # inputs=reports_table,
+ # outputs=reports_checkboxes
+ # )
+
+ # Initialize both the table and checkboxes on page load
+ def init_reports_ui():
+ print("Initializing reports UI...")
+ reports_data = self._get_reports_for_display()
+ choices = self._get_report_choices(reports_data)
+
+ # Log the actual choices for debugging
+ print(f"Initializing reports UI with {len(reports_data)} reports and {len(choices)} choices")
+ for i, choice in enumerate(choices[:5]):
+ print(f"Sample choice {i}: {choice}")
+ if len(choices) > 5:
+ print(f"...and {len(choices) - 5} more choices")
+
+ status = "Reports management initialized successfully."
+ return reports_data, choices, status
+
+ interface.load(
+ fn=init_reports_ui,
+ inputs=[],
+ outputs=[reports_table, reports_checkboxes, status_message]
+ )
+
return interface
def launch(self, **kwargs):