| import gradio as gr |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| from agent.supplier_agent import SupplierAgent |
| from agent.utils import DataValidator, ReportFormatter |
| import io |
| from PIL import Image |
| import os |
| import tempfile |
| import json |
|
|
| class SupplierAnalysisApp: |
| def __init__(self): |
| self.api_key = None |
| self.analysis_results = None |
| self.current_agent = None |
| self.chat_history = [] |
|
|
| def validate_api_key(self, api_key): |
| """Validate the OpenAI API key""" |
| if not api_key: |
| return "β οΈ Please enter your OpenAI API key" |
| self.api_key = api_key |
| return "β
API key set successfully" |
|
|
| def process_files(self, api_key, excel_file, audit_files): |
| """Process uploaded files and run analysis""" |
| if not api_key: |
| return "β οΈ Please enter your OpenAI API key first", None, None, None, None, None, None, None, None, None |
|
|
| if not excel_file: |
| return "β οΈ Please upload supplier data (Excel/CSV file)", None, None, None, None, None, None, None, None, None |
| |
| if not audit_files: |
| return "β οΈ Please upload at least one audit report (Word document)", None, None, None, None, None, None, None, None, None |
|
|
| try: |
| |
| self.current_agent = SupplierAgent(api_key) |
| |
| |
| excel_path = str(excel_file) |
| |
| |
| audit_paths = [str(f) for f in audit_files] if audit_files else [] |
| |
| print(f"Debug - excel_path: {excel_path}") |
| print(f"Debug - audit_paths: {audit_paths}") |
| |
| |
| if not os.path.exists(excel_path): |
| return f"β οΈ Excel file not found: {excel_path}", None, None, None, None, None, None, None, None, None, None, None, None |
| |
| for audit_path in audit_paths: |
| if not os.path.exists(audit_path): |
| return f"β οΈ Audit file not found: {audit_path}", None, None, None, None, None, None, None, None, None, None, None, None |
| |
| |
| class FileWithName: |
| def __init__(self, path): |
| self.name = path |
| self.path = path |
| self._file = None |
| |
| def _ensure_file_open(self): |
| if self._file is None: |
| self._file = open(self.path, 'rb') |
| |
| def read(self, size=-1): |
| self._ensure_file_open() |
| return self._file.read(size) |
| |
| def seek(self, pos, whence=0): |
| self._ensure_file_open() |
| return self._file.seek(pos, whence) |
| |
| def tell(self): |
| self._ensure_file_open() |
| return self._file.tell() |
| |
| def seekable(self): |
| return True |
| |
| def readable(self): |
| return True |
| |
| def writable(self): |
| return False |
| |
| def close(self): |
| if self._file is not None: |
| self._file.close() |
| self._file = None |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, exc_type, exc_val, exc_tb): |
| self.close() |
| |
| def __getattr__(self, name): |
| |
| self._ensure_file_open() |
| return getattr(self._file, name) |
| |
| |
| excel_file_obj = FileWithName(excel_path) |
| audit_file_objs = [FileWithName(path) for path in audit_paths] |
| |
| |
| result = self.current_agent.process_supplier_data(excel_file_obj, audit_file_objs) |
| |
| if not result["success"]: |
| return f"β Analysis failed: {result['error']}", None, None, None, None, None, None, None, None, None |
| |
| self.analysis_results = result["data"] |
| |
| |
| scores = self.analysis_results["score_table"] |
| |
| |
| fig = go.Figure(data=[ |
| go.Bar( |
| x=list(scores.keys()), |
| y=list(scores.values()), |
| marker_color='lightblue', |
| marker_line_color='navy', |
| marker_line_width=2 |
| ) |
| ]) |
| |
| fig.update_layout( |
| title="Supplier Performance Scores", |
| xaxis_title="Suppliers", |
| yaxis_title="Performance Score", |
| template="plotly_white" |
| ) |
| |
| |
| avg_score = round(sum(scores.values()) / len(scores), 2) |
| best_supplier = max(scores, key=scores.get) |
| best_score = scores[best_supplier] |
| |
| |
| weights = self.analysis_results.get("weights", {}) |
| weights_formatted = "\n".join([f"β’ **{metric}**: {weight:.1%}" for metric, weight in weights.items()]) |
| |
| |
| ai_summary = self.analysis_results.get("summary", "No summary available.") |
| |
| |
| scores_df = ReportFormatter.format_score_table(scores) |
| scores_html = scores_df.to_html(index=False, classes="scores-table", table_id="scores-table") |
| |
| |
| audit_findings = self.analysis_results.get("audit_findings", {}) |
| if audit_findings: |
| findings_df = ReportFormatter.format_findings_summary(audit_findings) |
| findings_html = findings_df.to_html(index=False, classes="findings-table", table_id="findings-table") |
| else: |
| findings_html = "<p>No audit findings available.</p>" |
| |
| return ( |
| "β
Analysis completed successfully!", |
| fig, |
| len(scores), |
| avg_score, |
| best_supplier, |
| best_score, |
| weights_formatted, |
| ai_summary, |
| scores_html, |
| findings_html |
| ) |
| |
| except Exception as e: |
| return f"β Error during analysis: {str(e)}", None, None, None, None, None, None, None, None, None |
|
|
| def chat_with_ai(self, question): |
| """Handle chat interactions""" |
| if not self.analysis_results: |
| return "β οΈ No analysis data available. Please run analysis first." |
| |
| if not self.api_key: |
| return "β οΈ Please enter your OpenAI API key first." |
| |
| if not question.strip(): |
| return "β οΈ Please enter a question." |
| |
| try: |
| from langchain_openai import ChatOpenAI |
| |
| |
| context = self._create_data_context() |
| |
| llm = ChatOpenAI(model="gpt-3.5-turbo", api_key=self.api_key) |
| |
| prompt = f""" |
| You are a supplier management expert analyzing performance data. Answer the user's question based on the supplier data provided. |
| |
| SUPPLIER DATA CONTEXT: |
| {context} |
| |
| USER QUESTION: {question} |
| |
| Guidelines for your response: |
| - Be specific and reference actual data from the context |
| - Provide actionable insights and recommendations |
| - Use supplier names and specific scores/metrics when relevant |
| - Keep responses concise but comprehensive |
| - If the question can't be answered from the data, say so clearly |
| - Focus on business value and practical next steps |
| |
| Answer: |
| """ |
| |
| result = llm.invoke(prompt) |
| response = result.content |
| |
| |
| self.chat_history.append((question, response)) |
| |
| |
| formatted_history = "" |
| for i, (q, a) in enumerate(self.chat_history, 1): |
| formatted_history += f"**Question {i}:** {q}\n\n**Answer:** {a}\n\n---\n\n" |
| |
| return formatted_history |
| |
| except Exception as e: |
| return f"β Error: {str(e)}" |
|
|
| def chat_with_suggested_question(self, question): |
| """Handle suggested question clicks""" |
| return self.chat_with_ai(question) |
|
|
| def clear_chat(self): |
| """Clear chat history""" |
| self.chat_history = [] |
| return "" |
|
|
| def _create_data_context(self): |
| """Create context for AI chat""" |
| scores = self.analysis_results.get("score_table", {}) |
| findings = self.analysis_results.get("audit_findings", {}) |
| weights = self.analysis_results.get("weights", {}) |
| summary = self.analysis_results.get("summary", "") |
| supplier_data = self.analysis_results.get("structured_data", []) |
| |
| context = f""" |
| SUPPLIER PERFORMANCE ANALYSIS CONTEXT: |
| |
| PERFORMANCE SCORES: |
| {', '.join([f"{supplier}: {score}" for supplier, score in scores.items()])} |
| |
| AUDIT FINDINGS: |
| {', '.join([f"{supplier}: {count} findings" for supplier, count in findings.items()])} |
| |
| PERFORMANCE WEIGHTS USED: |
| {', '.join([f"{metric}: {weight:.1%}" for metric, weight in weights.items()])} |
| |
| DETAILED SUPPLIER DATA: |
| """ |
| |
| for supplier in supplier_data: |
| context += f"\n{supplier.get('Supplier', 'Unknown')}: " |
| context += f"On-Time Delivery: {supplier.get('OnTimeDeliveryRate', 'N/A')}%, " |
| context += f"Defect Rate: {supplier.get('DefectRate', 'N/A')}%" |
| |
| context += f"\n\nAI GENERATED SUMMARY:\n{summary}" |
| |
| return context |
|
|
| def generate_report(self): |
| """Generate and return Word report""" |
| if not self.current_agent or not self.analysis_results: |
| return None |
| |
| try: |
| doc_bytes = self.current_agent.save_to_doc(self.analysis_results) |
| |
| report_temp = tempfile.NamedTemporaryFile(delete=False, suffix='.docx') |
| with open(report_temp.name, 'wb') as f: |
| f.write(doc_bytes) |
| return report_temp.name |
| except Exception: |
| return None |
|
|
|
|
|
|
| def main(): |
| app = SupplierAnalysisApp() |
| |
| |
| css = """ |
| /* Light mode (default) */ |
| .gradio-container { |
| --bg-primary: #ffffff; |
| --bg-secondary: #f8fafc; |
| --bg-tertiary: #f1f5f9; |
| --text-primary: #1e293b; |
| --text-secondary: #475569; |
| --text-tertiary: #64748b; |
| --border-primary: #e2e8f0; |
| --border-secondary: #cbd5e1; |
| --shadow-light: rgba(0, 0, 0, 0.05); |
| --shadow-medium: rgba(0, 0, 0, 0.1); |
| max-width: 100% !important; |
| padding: 0 !important; |
| } |
| |
| /* Dark mode */ |
| .dark .gradio-container, |
| [data-theme="dark"] .gradio-container, |
| .gradio-container.dark { |
| --bg-primary: #1e293b; |
| --bg-secondary: #334155; |
| --bg-tertiary: #475569; |
| --text-primary: #f1f5f9; |
| --text-secondary: #cbd5e1; |
| --text-tertiary: #94a3b8; |
| --border-primary: #475569; |
| --border-secondary: #64748b; |
| --shadow-light: rgba(0, 0, 0, 0.2); |
| --shadow-medium: rgba(0, 0, 0, 0.3); |
| } |
| |
| /* Auto-detect system dark mode */ |
| @media (prefers-color-scheme: dark) { |
| .gradio-container:not(.light) { |
| --bg-primary: #1e293b; |
| --bg-secondary: #334155; |
| --bg-tertiary: #475569; |
| --text-primary: #f1f5f9; |
| --text-secondary: #cbd5e1; |
| --text-tertiary: #94a3b8; |
| --border-primary: #475569; |
| --border-secondary: #64748b; |
| --shadow-light: rgba(0, 0, 0, 0.2); |
| --shadow-medium: rgba(0, 0, 0, 0.3); |
| } |
| } |
| |
| /* Container layout adjustments */ |
| .gradio-container .main { |
| gap: 1rem !important; |
| padding: 1rem !important; |
| } |
| |
| .main-content { |
| background: var(--bg-secondary) !important; |
| padding: 1.5rem !important; |
| border-radius: 16px !important; |
| border: 1px solid var(--border-primary) !important; |
| box-shadow: 0 4px 6px var(--shadow-light) !important; |
| margin-left: 1rem !important; |
| width: 100% !important; |
| min-height: 800px !important; |
| } |
| |
| /* Improved metrics grid */ |
| .metrics-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important; |
| gap: 1rem !important; |
| margin: 1rem 0 !important; |
| } |
| |
| /* Section cards */ |
| .section-card { |
| background: var(--bg-primary); |
| border: 1px solid var(--border-primary); |
| border-radius: 16px; |
| padding: 1.5rem; |
| margin: 1rem 0; |
| box-shadow: 0 2px 4px var(--shadow-light); |
| transition: all 0.2s ease; |
| width: 100% !important; |
| } |
| |
| .section-card:hover { |
| box-shadow: 0 6px 12px var(--shadow-medium); |
| border-color: var(--border-secondary); |
| transform: translateY(-1px); |
| } |
| |
| .section-header { |
| font-size: 1.5rem; |
| font-weight: 600; |
| color: var(--text-primary); |
| margin-bottom: 1.5rem; |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| border-bottom: 1px solid var(--border-primary); |
| padding-bottom: 1rem; |
| letter-spacing: -0.025em; |
| } |
| |
| /* Upload section */ |
| .upload-section { |
| background: var(--bg-primary); |
| border: 2px dashed var(--border-secondary); |
| border-radius: 16px; |
| padding: 2rem !important; |
| margin: 1rem 0 !important; |
| transition: all 0.3s ease; |
| text-align: center; |
| width: 100% !important; |
| } |
| |
| .upload-section:hover { |
| border-color: #3b82f6; |
| background: rgba(59, 130, 246, 0.05); |
| } |
| |
| /* Make plots and tables utilize full width */ |
| .gradio-plot, .scores-table, .findings-table { |
| width: 100% !important; |
| max-width: none !important; |
| } |
| |
| /* Adjust chat section for better space utilization */ |
| .chat-section { |
| width: 100% !important; |
| max-width: none !important; |
| } |
| |
| /* Header adjustments */ |
| .app-header { |
| margin: 1rem !important; |
| padding: 2.5rem 2rem !important; |
| width: calc(100% - 2rem) !important; |
| } |
| |
| .app-header { |
| background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); |
| padding: 3rem 2rem; |
| border-radius: 16px; |
| text-align: center; |
| margin-bottom: 2rem; |
| color: white; |
| box-shadow: 0 8px 32px rgba(59, 130, 246, 0.15); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| .app-title { |
| font-size: 2.75rem; |
| font-weight: 600; |
| margin-bottom: 0.75rem; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| letter-spacing: -0.025em; |
| } |
| .app-subtitle { |
| font-size: 1.125rem; |
| opacity: 0.9; |
| margin-bottom: 0; |
| font-weight: 400; |
| letter-spacing: 0.025em; |
| } |
| .sidebar { |
| background: var(--bg-secondary) !important; |
| padding: 2rem !important; |
| border-radius: 16px !important; |
| min-height: 800px !important; |
| border: 1px solid var(--border-primary) !important; |
| box-shadow: 0 4px 6px var(--shadow-light) !important; |
| } |
| .sidebar h3 { |
| color: var(--text-primary) !important; |
| margin-bottom: 1.5rem !important; |
| font-size: 1.25rem !important; |
| font-weight: 600 !important; |
| display: flex !important; |
| align-items: center !important; |
| gap: 0.75rem !important; |
| letter-spacing: -0.025em !important; |
| } |
| .nav-button { |
| width: 100% !important; |
| margin-bottom: 0.75rem !important; |
| text-align: left !important; |
| background: var(--bg-primary) !important; |
| border: 1px solid var(--border-primary) !important; |
| color: var(--text-secondary) !important; |
| padding: 14px 18px !important; |
| border-radius: 12px !important; |
| transition: all 0.2s ease !important; |
| font-weight: 500 !important; |
| box-shadow: 0 1px 3px var(--shadow-light) !important; |
| } |
| .nav-button:hover { |
| background: var(--bg-tertiary) !important; |
| border-color: var(--border-secondary) !important; |
| transform: translateY(-1px) !important; |
| box-shadow: 0 4px 6px var(--shadow-light) !important; |
| } |
| .nav-button.active { |
| background: rgba(59, 130, 246, 0.15) !important; |
| border-color: #3b82f6 !important; |
| color: #3b82f6 !important; |
| box-shadow: 0 4px 6px rgba(59, 130, 246, 0.15) !important; |
| } |
| .info-banner { |
| background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); |
| border: 1px solid #bfdbfe; |
| padding: 1.25rem 1.5rem; |
| border-radius: 12px; |
| margin: 1.5rem 0; |
| text-align: center; |
| color: #1e40af; |
| font-weight: 500; |
| box-shadow: 0 1px 3px rgba(59, 130, 246, 0.1); |
| } |
| .metric-card { |
| background: rgba(59, 130, 246, 0.08); |
| border: 1px solid rgba(59, 130, 246, 0.2); |
| border-radius: 12px; |
| padding: 1.5rem; |
| text-align: center; |
| transition: all 0.2s ease; |
| box-shadow: 0 1px 3px var(--shadow-light); |
| } |
| .metric-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 6px var(--shadow-medium); |
| border-color: rgba(59, 130, 246, 0.4); |
| } |
| .metric-label { |
| font-size: 0.875rem; |
| color: var(--text-tertiary); |
| font-weight: 500; |
| margin-bottom: 0.75rem; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| .metric-value { |
| font-size: 2rem; |
| font-weight: 700; |
| color: var(--text-primary); |
| letter-spacing: -0.025em; |
| } |
| .scores-table, .findings-table { |
| width: 100%; |
| border-collapse: collapse; |
| margin: 1.5rem 0; |
| background: var(--bg-primary); |
| border-radius: 12px; |
| overflow: hidden; |
| box-shadow: 0 1px 3px var(--shadow-light); |
| border: 1px solid var(--border-primary); |
| } |
| .scores-table th, .findings-table th { |
| background: var(--bg-secondary); |
| color: var(--text-secondary); |
| font-weight: 600; |
| padding: 16px 20px; |
| text-align: left; |
| border-bottom: 1px solid var(--border-primary); |
| font-size: 0.875rem; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| .scores-table td, .findings-table td { |
| padding: 16px 20px; |
| border-bottom: 1px solid var(--border-primary); |
| color: var(--text-secondary); |
| font-weight: 500; |
| } |
| .scores-table tr:last-child td, .findings-table tr:last-child td { |
| border-bottom: none; |
| } |
| .scores-table tr:hover, .findings-table tr:hover { |
| background: rgba(59, 130, 246, 0.05); |
| } |
| .gradio-button[variant="primary"] { |
| background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; |
| border: none !important; |
| color: white !important; |
| font-weight: 600 !important; |
| padding: 12px 24px !important; |
| border-radius: 12px !important; |
| box-shadow: 0 4px 6px rgba(59, 130, 246, 0.25) !important; |
| transition: all 0.2s ease !important; |
| } |
| .gradio-button[variant="primary"]:hover { |
| transform: translateY(-1px) !important; |
| box-shadow: 0 6px 8px rgba(59, 130, 246, 0.3) !important; |
| } |
| .gradio-textbox, .gradio-file { |
| border-radius: 12px !important; |
| border-color: var(--border-primary) !important; |
| box-shadow: 0 1px 3px var(--shadow-light) !important; |
| background: var(--bg-primary) !important; |
| color: var(--text-primary) !important; |
| } |
| .gradio-textbox:focus, .gradio-file:focus-within { |
| border-color: #3b82f6 !important; |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important; |
| } |
| .analysis-results { |
| margin-top: 1.5rem; |
| padding-top: 1.5rem; |
| border-top: 1px solid var(--border-primary); |
| } |
| |
| /* Ensure text inputs are properly styled in dark mode */ |
| .gradio-textbox input, .gradio-textbox textarea { |
| background: var(--bg-primary) !important; |
| color: var(--text-primary) !important; |
| border-color: var(--border-primary) !important; |
| } |
| |
| /* File upload area styling */ |
| .gradio-file { |
| background: var(--bg-primary) !important; |
| color: var(--text-primary) !important; |
| } |
| |
| /* Ensure labels are visible in dark mode */ |
| label, .gradio-label { |
| color: var(--text-secondary) !important; |
| } |
| |
| /* Additional text elements */ |
| .gradio-markdown p, .gradio-html { |
| color: var(--text-primary) !important; |
| } |
| |
| /* Fix all text visibility issues in dark mode */ |
| .gradio-textbox input::placeholder, |
| .gradio-textbox textarea::placeholder { |
| color: var(--text-tertiary) !important; |
| opacity: 0.7 !important; |
| } |
| |
| /* Ensure all body text is readable */ |
| .gradio-container * { |
| color: var(--text-primary) !important; |
| } |
| |
| /* Override for specific elements that should keep their colors */ |
| .app-header, .app-header * { |
| color: white !important; |
| } |
| |
| .info-banner, .info-banner * { |
| color: #1e40af !important; |
| } |
| |
| .nav-button.active, .nav-button.active * { |
| color: #3b82f6 !important; |
| } |
| |
| /* Gradio specific text elements */ |
| .gradio-textbox label, |
| .gradio-file label, |
| .gradio-number label, |
| .gradio-markdown label, |
| .gradio-plot label, |
| .gradio-html label, |
| .gradio-button { |
| color: var(--text-primary) !important; |
| } |
| |
| /* Form descriptions and help text */ |
| .gradio-form .description, |
| .gradio-textbox .description, |
| .gradio-file .description { |
| color: var(--text-tertiary) !important; |
| } |
| |
| /* Ensure status messages are visible */ |
| .gradio-textbox input[readonly], |
| .gradio-textbox textarea[readonly] { |
| color: var(--text-secondary) !important; |
| background: var(--bg-tertiary) !important; |
| } |
| |
| /* File upload text */ |
| .gradio-file .file-preview, |
| .gradio-file .upload-text { |
| color: var(--text-primary) !important; |
| } |
| |
| /* Secondary buttons */ |
| .gradio-button[variant="secondary"] { |
| background: var(--bg-tertiary) !important; |
| border: 1px solid var(--border-primary) !important; |
| color: var(--text-primary) !important; |
| font-weight: 500 !important; |
| padding: 10px 20px !important; |
| border-radius: 8px !important; |
| } |
| |
| .gradio-button[variant="secondary"]:hover { |
| background: var(--bg-primary) !important; |
| border-color: var(--border-secondary) !important; |
| } |
| |
| /* Number input styling */ |
| .gradio-number input { |
| background: var(--bg-primary) !important; |
| color: var(--text-primary) !important; |
| border-color: var(--border-primary) !important; |
| } |
| |
| /* Plot/chart container */ |
| .gradio-plot { |
| background: var(--bg-primary) !important; |
| border: 1px solid var(--border-primary) !important; |
| border-radius: 12px !important; |
| } |
| |
| /* Ensure all interactive elements are visible */ |
| button, input, textarea, select { |
| color: var(--text-primary) !important; |
| } |
| |
| /* Fix any remaining text contrast issues */ |
| p, span, div, h1, h2, h3, h4, h5, h6 { |
| color: inherit !important; |
| } |
| |
| /* Special handling for custom HTML content */ |
| .section-header * { |
| color: var(--text-primary) !important; |
| } |
| |
| /* Upload area text */ |
| .upload-section * { |
| color: var(--text-secondary) !important; |
| } |
| |
| /* Metric card text is already handled, but ensure consistency */ |
| .metric-card .metric-label { |
| color: var(--text-tertiary) !important; |
| } |
| |
| .metric-card .metric-value { |
| color: var(--text-primary) !important; |
| } |
| |
| /* Remove first section card margin to align with header */ |
| .section-card:first-child { |
| margin-top: 0; |
| } |
| |
| /* Sidebar width control */ |
| .sidebar { |
| max-width: 280px !important; |
| min-width: 250px !important; |
| width: 100% !important; |
| } |
| |
| /* Enhanced button styling */ |
| .action-button { |
| background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; |
| color: white !important; |
| font-weight: 600 !important; |
| padding: 14px 28px !important; |
| border-radius: 12px !important; |
| box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25) !important; |
| transition: all 0.2s ease !important; |
| border: none !important; |
| cursor: pointer !important; |
| position: relative !important; |
| overflow: hidden !important; |
| } |
| |
| .action-button::before { |
| content: "" !important; |
| position: absolute !important; |
| top: 0 !important; |
| left: 0 !important; |
| width: 100% !important; |
| height: 100% !important; |
| background: linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0)) !important; |
| opacity: 0 !important; |
| transition: opacity 0.2s ease !important; |
| } |
| |
| .action-button:hover { |
| transform: translateY(-2px) !important; |
| box-shadow: 0 6px 16px rgba(59, 130, 246, 0.35) !important; |
| } |
| |
| .action-button:hover::before { |
| opacity: 1 !important; |
| } |
| |
| .action-button:active { |
| transform: translateY(1px) !important; |
| box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2) !important; |
| } |
| |
| /* Chat suggestion buttons */ |
| .suggestion-button { |
| background: rgba(59, 130, 246, 0.1) !important; |
| color: #3b82f6 !important; |
| font-weight: 500 !important; |
| padding: 12px 20px !important; |
| border-radius: 10px !important; |
| border: 1px solid rgba(59, 130, 246, 0.2) !important; |
| transition: all 0.2s ease !important; |
| cursor: pointer !important; |
| width: 100% !important; |
| text-align: left !important; |
| margin-bottom: 8px !important; |
| display: flex !important; |
| align-items: center !important; |
| gap: 8px !important; |
| } |
| |
| .suggestion-button:hover { |
| background: rgba(59, 130, 246, 0.15) !important; |
| border-color: rgba(59, 130, 246, 0.4) !important; |
| transform: translateY(-1px) !important; |
| box-shadow: 0 4px 8px rgba(59, 130, 246, 0.15) !important; |
| } |
| |
| .suggestion-button:active { |
| transform: translateY(0) !important; |
| box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1) !important; |
| } |
| |
| /* Download section styling */ |
| .download-section { |
| display: flex !important; |
| align-items: center !important; |
| gap: 16px !important; |
| background: rgba(59, 130, 246, 0.05) !important; |
| padding: 20px !important; |
| border-radius: 12px !important; |
| border: 1px dashed rgba(59, 130, 246, 0.2) !important; |
| margin-top: 16px !important; |
| } |
| |
| .download-section .action-button { |
| flex-shrink: 0 !important; |
| } |
| |
| /* Ensure all buttons have pointer cursor */ |
| .gradio-button { |
| cursor: pointer !important; |
| } |
| """ |
| |
| def switch_page(page): |
| """Switch between different pages""" |
| if page == "analysis": |
| return ( |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(variant="primary", elem_classes="nav-button active"), |
| gr.update(variant="secondary", elem_classes="nav-button") |
| ) |
| else: |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(variant="secondary", elem_classes="nav-button"), |
| gr.update(variant="primary", elem_classes="nav-button active") |
| ) |
| |
| |
| with gr.Blocks(title="Supplier Management Agent", theme=gr.themes.Default(), css=css) as interface: |
| |
| gr.HTML(""" |
| <div class="app-header"> |
| <div class="app-title">π Supplier Management Agent</div> |
| <div class="app-subtitle">AI-Powered Supplier Performance Analysis using LangGraph and Gradio</div> |
| </div> |
| """) |
| |
| with gr.Row(): |
| |
| with gr.Column(scale=1, elem_classes="sidebar", min_width=250): |
| gr.HTML('<h3>π§ Navigation</h3>') |
| |
| analysis_nav_btn = gr.Button("π Analysis", variant="primary", elem_classes="nav-button active") |
| chat_nav_btn = gr.Button("π¬ Chat with AI", variant="secondary", elem_classes="nav-button") |
| |
| gr.HTML('<hr style="border-color: #e2e8f0; margin: 2rem 0;">') |
| |
| |
| gr.HTML('<h3>π API Configuration</h3>') |
| api_key_input = gr.Textbox( |
| label="OpenAI API Key", |
| placeholder="Enter your OpenAI API key", |
| type="password", |
| container=True |
| ) |
| api_status = gr.Textbox(label="Status", interactive=False, container=True) |
| |
| |
| with gr.Column(scale=8, elem_classes="main-content"): |
| |
| with gr.Group(visible=True) as analysis_section: |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π Upload Your Data</div>') |
| gr.HTML('<div class="info-banner">π Upload your supplier data and audit reports to get started with AI-powered analysis!</div>') |
| |
| with gr.Row(): |
| with gr.Column(): |
| gr.HTML('<div style="font-size: 1.125rem; font-weight: 600; color: #374151; margin-bottom: 1rem;">π Supplier Data</div>') |
| excel_file = gr.File( |
| label="Upload Excel/CSV file", |
| file_types=[".xlsx", ".xls", ".csv"], |
| height=120, |
| elem_classes="upload-section" |
| ) |
| with gr.Column(): |
| gr.HTML('<div style="font-size: 1.125rem; font-weight: 600; color: #374151; margin-bottom: 1rem;">π Audit Reports</div>') |
| audit_files = gr.File( |
| label="Upload Word documents", |
| file_types=[".docx"], |
| file_count="multiple", |
| height=120, |
| elem_classes="upload-section" |
| ) |
| |
| |
| with gr.Row(): |
| analyze_btn = gr.Button("π Run Analysis", elem_classes="action-button", size="lg") |
| analysis_status = gr.Textbox(label="Analysis Status", interactive=False) |
| |
| |
| with gr.Group(visible=False, elem_classes="analysis-results") as results_container: |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π Key Metrics</div>') |
| with gr.Row(elem_classes="metrics-grid"): |
| with gr.Column(elem_classes="metric-card"): |
| total_suppliers = gr.Number(label="Total Suppliers", interactive=False) |
| with gr.Column(elem_classes="metric-card"): |
| avg_score = gr.Number(label="Average Score", interactive=False) |
| with gr.Column(elem_classes="metric-card"): |
| top_performer = gr.Textbox(label="Top Performer", interactive=False) |
| with gr.Column(elem_classes="metric-card"): |
| best_score = gr.Number(label="Best Score", interactive=False) |
| |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π Performance Scores</div>') |
| performance_plot = gr.Plot(label="Performance Chart") |
| |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">βοΈ Performance Weights</div>') |
| performance_weights = gr.Markdown(label="Weights Used in Analysis") |
| |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π€ AI-Generated Summary</div>') |
| ai_summary = gr.Markdown(label="Analysis Summary") |
| |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π Detailed Scores</div>') |
| detailed_scores = gr.HTML(label="Supplier Performance Details") |
| |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π Audit Findings Summary</div>') |
| audit_findings = gr.HTML(label="Audit Results Overview") |
| |
| |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π₯ Download Report</div>') |
| with gr.Row(elem_classes="download-section"): |
| report_btn = gr.Button("π Generate Word Report", elem_classes="action-button", size="lg") |
| report_download = gr.File(label="Download Report") |
| |
| |
| with gr.Group(visible=False) as chat_section: |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-header">π¬ Ask Questions About Your Supplier Data</div>') |
| |
| |
| with gr.Row(): |
| with gr.Column(): |
| chat_input = gr.Textbox( |
| label="Your Question", |
| placeholder="e.g., Which supplier should I focus on improving first?", |
| lines=2 |
| ) |
| with gr.Row(): |
| chat_btn = gr.Button("Send Question", elem_classes="action-button") |
| clear_btn = gr.Button("Clear Chat", variant="secondary") |
| |
| chat_output = gr.Markdown(label="Chat History", value="") |
| |
| |
| gr.HTML('<div style="font-size: 1.25rem; font-weight: 600; color: #374151; margin: 2rem 0 1rem 0;">π‘ Suggested Questions</div>') |
| suggested_questions = [ |
| "Who is my best performing supplier and why?", |
| "Which suppliers need immediate improvement?", |
| "What are the biggest risks in my supplier base?", |
| "How can I improve overall supplier performance?", |
| "What should be my top 3 priorities?", |
| "Which suppliers have the most audit findings?" |
| ] |
| |
| with gr.Row(): |
| for i in range(0, len(suggested_questions), 2): |
| with gr.Column(): |
| if i < len(suggested_questions): |
| q1_btn = gr.Button(f"π {suggested_questions[i]}", elem_classes="suggestion-button") |
| q1_btn.click( |
| fn=lambda q=suggested_questions[i]: app.chat_with_suggested_question(q), |
| outputs=chat_output |
| ) |
| if i + 1 < len(suggested_questions): |
| q2_btn = gr.Button(f"π {suggested_questions[i+1]}", elem_classes="suggestion-button") |
| q2_btn.click( |
| fn=lambda q=suggested_questions[i+1]: app.chat_with_suggested_question(q), |
| outputs=chat_output |
| ) |
| |
| |
| api_key_input.change( |
| app.validate_api_key, |
| inputs=[api_key_input], |
| outputs=[api_status] |
| ) |
| |
| |
| analysis_nav_btn.click( |
| fn=lambda: switch_page("analysis"), |
| outputs=[analysis_section, chat_section, analysis_nav_btn, chat_nav_btn] |
| ) |
| |
| chat_nav_btn.click( |
| fn=lambda: switch_page("chat"), |
| outputs=[analysis_section, chat_section, analysis_nav_btn, chat_nav_btn] |
| ) |
| |
| |
| def analyze_and_show_results(api_key, excel_file, audit_files): |
| result = app.process_files(api_key, excel_file, audit_files) |
| |
| success = result[0].startswith("β
") if result[0] else False |
| return result + (gr.update(visible=success),) |
| |
| analyze_btn.click( |
| analyze_and_show_results, |
| inputs=[api_key_input, excel_file, audit_files], |
| outputs=[analysis_status, performance_plot, total_suppliers, avg_score, top_performer, best_score, performance_weights, ai_summary, detailed_scores, audit_findings, results_container] |
| ) |
| |
| chat_btn.click( |
| app.chat_with_ai, |
| inputs=[chat_input], |
| outputs=[chat_output] |
| ) |
| |
| clear_btn.click( |
| app.clear_chat, |
| outputs=[chat_output] |
| ) |
| |
| report_btn.click( |
| app.generate_report, |
| outputs=[report_download] |
| ) |
| |
|
|
| |
| |
| interface.launch(share=False) |
|
|
| if __name__ == "__main__": |
| main() |
|
|