diff --git a/ui/gradio_interface.py b/ui/gradio_interface.py index 2add6e9..9f476cc 100644 --- a/ui/gradio_interface.py +++ b/ui/gradio_interface.py @@ -35,8 +35,21 @@ class GradioInterface: self.sub_question_executor = get_sub_question_executor() self.results_dir = Path(__file__).parent.parent / "results" self.results_dir.mkdir(exist_ok=True) - self.reports_dir = Path(__file__).parent.parent + + # Create a dedicated reports directory with subdirectories + self.reports_dir = Path(__file__).parent.parent / "reports" self.reports_dir.mkdir(exist_ok=True) + + # Create daily subdirectory for organization + self.reports_daily_dir = self.reports_dir / datetime.now().strftime("%Y-%m-%d") + self.reports_daily_dir.mkdir(exist_ok=True) + + # Create a metadata file to track reports + self.reports_metadata_file = self.reports_dir / "reports_metadata.json" + if not self.reports_metadata_file.exists(): + with open(self.reports_metadata_file, "w") as f: + json.dump({"reports": []}, f, indent=2) + self.detail_level_manager = get_report_detail_level_manager() self.config = Config() @@ -206,7 +219,7 @@ class GradioInterface: Path to the generated report """ try: - # Create a timestamped output file + # Create a timestamped output file in the daily directory timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") model_suffix = "" @@ -225,7 +238,12 @@ class GradioInterface: print(f"Extracted model name: {custom_model}") print(f"Using model suffix: {model_suffix}") - output_file = self.reports_dir / f"report_{timestamp}{model_suffix}.md" + # Create a unique report ID + import hashlib + report_id = f"{timestamp}_{hashlib.md5(query.encode()).hexdigest()[:8]}" + + # Define the output file path in the daily directory + output_file = self.reports_daily_dir / f"report_{report_id}{model_suffix}.md" # Get detail level configuration config = self.detail_level_manager.get_detail_level_config(detail_level) @@ -256,10 +274,6 @@ class GradioInterface: default_model = detail_config.get("model", "unknown") print(f"Default model for {detail_level} detail level: {default_model}") - # First set the detail level, which will set the default model for this detail level - self.report_generator.set_detail_level(detail_level) - print(f"After setting detail level, report generator model is: {self.report_generator.model_name}") - # Then explicitly override with custom model if provided if custom_model: # Extract the actual model name from the display name format @@ -523,6 +537,19 @@ class GradioInterface: print(f"Report saved to: {output_file}") + # Update report metadata + self._update_report_metadata(report_id, { + "id": report_id, + "timestamp": timestamp, + "query": query, + "detail_level": detail_level, + "query_type": query_type, + "model": custom_model if custom_model else config.get("model", "default"), + "file_path": str(output_file), + "file_size": output_file.stat().st_size, + "creation_date": datetime.now().isoformat() + }) + return report, str(output_file) except Exception as e: @@ -545,6 +572,107 @@ class GradioInterface: # Remove content between and tags import re return re.sub(r'.*?', '', text, flags=re.DOTALL) + + def _update_report_metadata(self, report_id, metadata): + """ + Update the report metadata file with new report information. + + Args: + report_id (str): Unique identifier for the report + metadata (dict): Report metadata to store + """ + try: + # Load existing metadata + with open(self.reports_metadata_file, 'r') as f: + all_metadata = json.load(f) + + # Check if report already exists + existing_report = None + for i, report in enumerate(all_metadata.get('reports', [])): + if report.get('id') == report_id: + existing_report = i + break + + # Update or add the report metadata + if existing_report is not None: + all_metadata['reports'][existing_report] = metadata + else: + all_metadata['reports'].append(metadata) + + # Save updated metadata + with open(self.reports_metadata_file, 'w') as f: + json.dump(all_metadata, f, indent=2) + + print(f"Updated metadata for report {report_id}") + + except Exception as e: + print(f"Error updating report metadata: {str(e)}") + + def get_all_reports(self): + """ + Get all report metadata. + + Returns: + list: List of report metadata dictionaries + """ + try: + # Load metadata + with open(self.reports_metadata_file, 'r') as f: + all_metadata = json.load(f) + + # Return reports sorted by creation date (newest first) + reports = all_metadata.get('reports', []) + return sorted(reports, key=lambda x: x.get('creation_date', ''), reverse=True) + + except Exception as e: + print(f"Error getting report metadata: {str(e)}") + return [] + + def delete_report(self, report_id): + """ + Delete a report and its metadata. + + Args: + report_id (str): ID of the report to delete + + Returns: + bool: True if successful, False otherwise + """ + try: + # Load metadata + with open(self.reports_metadata_file, 'r') as f: + all_metadata = json.load(f) + + # Find the report + report_to_delete = None + for report in all_metadata.get('reports', []): + if report.get('id') == report_id: + report_to_delete = report + break + + if not report_to_delete: + print(f"Report {report_id} not found") + return False + + # Delete the report file + file_path = report_to_delete.get('file_path') + if file_path and Path(file_path).exists(): + Path(file_path).unlink() + print(f"Deleted report file: {file_path}") + + # Remove from metadata + all_metadata['reports'] = [r for r in all_metadata.get('reports', []) if r.get('id') != report_id] + + # Save updated metadata + with open(self.reports_metadata_file, 'w') as f: + json.dump(all_metadata, f, indent=2) + + print(f"Deleted report {report_id} from metadata") + return True + + except Exception as e: + print(f"Error deleting report: {str(e)}") + return False def get_available_models(self): """ @@ -600,6 +728,331 @@ class GradioInterface: self.model_name_to_description = model_name_to_description return descriptions + + def _get_reports_for_display(self): + """Get reports formatted for display in the UI""" + reports = self.get_all_reports() + display_data = [] + + for report in reports: + # Format timestamp for display + timestamp = report.get('timestamp', '') + creation_date = report.get('creation_date', '') + if creation_date: + try: + # Convert ISO format to datetime and format for display + dt = datetime.fromisoformat(creation_date) + formatted_date = dt.strftime('%Y-%m-%d %H:%M:%S') + except: + formatted_date = creation_date + else: + formatted_date = timestamp + + # Format file size + file_size = report.get('file_size', 0) + if file_size < 1024: + formatted_size = f"{file_size} B" + elif file_size < 1024 * 1024: + formatted_size = f"{file_size / 1024:.1f} KB" + else: + formatted_size = f"{file_size / (1024 * 1024):.1f} MB" + + # Add row to display data + display_data.append([ + report.get('id', ''), + report.get('query', '')[:50] + ('...' if len(report.get('query', '')) > 50 else ''), + report.get('model', '').split('/')[-1], # Show only the model name without provider + report.get('detail_level', ''), + formatted_date, + formatted_size, + Path(report.get('file_path', '')).name, # Just the filename + ]) + + return display_data + + def _delete_selected_reports(self, selected_choices): + """Delete selected reports + + Args: + selected_choices (list): List of selected checkbox values in format "ID: Query (Model)" + + Returns: + tuple: Updated reports table data and updated checkbox choices + """ + if not selected_choices: + # If no reports are selected, just refresh the display + reports_data = self._get_reports_for_display() + choices = self._get_report_choices(reports_data) + return reports_data, choices, [] + + print(f"Selected choices for deletion: {selected_choices}") + + # Extract report IDs from selected choices + selected_report_ids = [] + for choice in selected_choices: + try: + # Split at the first colon to get the ID + if ':' in choice: + report_id = choice.split(':', 1)[0].strip() + selected_report_ids.append(report_id) + else: + print(f"Warning: Invalid choice format: {choice}") + except Exception as e: + print(f"Error processing choice {choice}: {e}") + + print(f"Deleting report IDs: {selected_report_ids}") + + # Delete selected reports + deleted_count = 0 + for report_id in selected_report_ids: + if self.delete_report(report_id): + deleted_count += 1 + print(f"Successfully deleted report: {report_id}") + else: + print(f"Failed to delete report: {report_id}") + + print(f"Deleted {deleted_count} reports") + + # Refresh the table and choices + reports_data = self._get_reports_for_display() + choices = self._get_report_choices(reports_data) + return reports_data, choices, [] + + def _download_selected_reports(self, selected_choices): + """Prepare selected reports for download + + Args: + selected_choices (list): List of selected checkbox values in format "ID: Query (Model)" + + Returns: + list: List of file paths to download + """ + if not selected_choices: + return [] + + print(f"Selected choices for download: {selected_choices}") + + # Extract report IDs from selected choices + selected_report_ids = [] + for choice in selected_choices: + try: + # Split at the first colon to get the ID + if ':' in choice: + report_id = choice.split(':', 1)[0].strip() + selected_report_ids.append(report_id) + else: + print(f"Warning: Invalid choice format: {choice}") + except Exception as e: + print(f"Error processing choice {choice}: {e}") + + print(f"Extracted report IDs: {selected_report_ids}") + + # Get file paths for selected reports + all_reports = self.get_all_reports() + files_to_download = [] + + for report_id in selected_report_ids: + report = next((r for r in all_reports if r.get('id') == report_id), None) + if report and "file_path" in report: + file_path = report["file_path"] + # Verify the file exists + if os.path.exists(file_path): + files_to_download.append(file_path) + print(f"Added file for download: {file_path}") + else: + print(f"Warning: File does not exist: {file_path}") + else: + print(f"Warning: Could not find report with ID {report_id}") + + return files_to_download + + def _get_report_choices(self, reports_data): + """Generate choices for the checkbox group based on reports data + + Args: + reports_data (list): List of report data rows + + Returns: + list: List of choices for the checkbox group in format "ID: Query (Model)" + """ + choices = [] + # If reports_data is empty, return an empty list + if not reports_data: + return [] + + # Get all reports from the metadata file to ensure IDs are available + all_reports = self.get_all_reports() + + # Create a mapping of report IDs to their full data + report_map = {report.get('id', ''): report for report in all_reports} + + for row in reports_data: + try: + report_id = row[0] + if not report_id: + continue + + # Get data from the table row + query = row[1] + model = row[2] + + # Format: "ID: Query (Model)" + choice_text = f"{report_id}: {query} ({model})" + choices.append(choice_text) + except (IndexError, TypeError) as e: + print(f"Error processing report row: {e}") + continue + + return choices + + def _cleanup_old_reports(self, days): + """Delete reports older than the specified number of days + + Args: + days (int): Number of days to keep reports for + + Returns: + list: Updated reports table data + """ + try: + if days <= 0: + print("Cleanup skipped - days parameter is 0 or negative") + return self._get_reports_for_display() + + # Calculate cutoff date + from datetime import timedelta + cutoff_date = datetime.now() - timedelta(days=days) + cutoff_str = cutoff_date.isoformat() + print(f"Cleaning up reports older than {cutoff_date.strftime('%Y-%m-%d %H:%M:%S')}") + + # Get all reports + all_reports = self.get_all_reports() + print(f"Found {len(all_reports)} total reports") + reports_to_delete = [] + + # Find reports older than cutoff date + for report in all_reports: + creation_date = report.get('creation_date', '') + if not creation_date: + print(f"Warning: Report {report.get('id')} has no creation date") + continue + + if creation_date < cutoff_str: + reports_to_delete.append(report.get('id')) + print(f"Marking report {report.get('id')} from {creation_date} for deletion") + + print(f"Found {len(reports_to_delete)} reports to delete") + + # Delete old reports + deleted_count = 0 + for report_id in reports_to_delete: + if self.delete_report(report_id): + deleted_count += 1 + + print(f"Successfully deleted {deleted_count} reports") + + # Refresh the table + updated_display = self._get_reports_for_display() + print(f"Returning updated display with {len(updated_display)} reports") + return updated_display + + except Exception as e: + print(f"Error in cleanup_old_reports: {e}") + import traceback + traceback.print_exc() + # Return current display data in case of error + return self._get_reports_for_display() + + def migrate_existing_reports(self): + """Migrate existing reports from the root directory to the reports directory structure + + Returns: + str: Status message indicating the result of the migration + """ + import re + import shutil + import os + + # Pattern to match report files like report_20250317_122351_llama-3.3-70b-versatile.md + report_pattern = re.compile(r'report_(?P\d{8})_(?P