kamalakn's picture
Upload 5 files
8d76a24 verified
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:
# Initialize agent
self.current_agent = SupplierAgent(api_key)
# Handle Gradio NamedString objects - they are the file paths themselves
excel_path = str(excel_file) # Convert NamedString to string
# Handle audit files - they come as a list of strings
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}")
# Verify files exist
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
# Create file objects with .name attribute that the agent expects
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):
# Delegate any other attributes to the underlying file object
self._ensure_file_open()
return getattr(self._file, name)
# Create file objects for the agent
excel_file_obj = FileWithName(excel_path)
audit_file_objs = [FileWithName(path) for path in audit_paths]
# Process data
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"]
# Generate visualizations
scores = self.analysis_results["score_table"]
# Performance chart
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"
)
# Create metrics
avg_score = round(sum(scores.values()) / len(scores), 2)
best_supplier = max(scores, key=scores.get)
best_score = scores[best_supplier]
# Format performance weights
weights = self.analysis_results.get("weights", {})
weights_formatted = "\n".join([f"β€’ **{metric}**: {weight:.1%}" for metric, weight in weights.items()])
# Get AI-generated summary
ai_summary = self.analysis_results.get("summary", "No summary available.")
# Format detailed scores table
scores_df = ReportFormatter.format_score_table(scores)
scores_html = scores_df.to_html(index=False, classes="scores-table", table_id="scores-table")
# Format audit findings summary
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), # total_suppliers
avg_score, # avg_score
best_supplier, # top_performer
best_score, # best_score
weights_formatted, # performance_weights
ai_summary, # ai_summary
scores_html, # detailed_scores
findings_html # audit_findings
)
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
# Create data context
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
# Add to chat history
self.chat_history.append((question, response))
# Format chat history for display
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)
# Create a temporary file for the report
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()
# Notion-inspired CSS styling with light blue accents and dark mode support
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), # analysis_section
gr.update(visible=False), # chat_section
gr.update(variant="primary", elem_classes="nav-button active"), # analysis_btn
gr.update(variant="secondary", elem_classes="nav-button") # chat_btn
)
else: # chat
return (
gr.update(visible=False), # analysis_section
gr.update(visible=True), # chat_section
gr.update(variant="secondary", elem_classes="nav-button"), # analysis_btn
gr.update(variant="primary", elem_classes="nav-button active") # chat_btn
)
# Define the interface
with gr.Blocks(title="Supplier Management Agent", theme=gr.themes.Default(), css=css) as interface:
# Header
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():
# Sidebar
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;">')
# API Key section in sidebar
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)
# Main content area - increased scale for wider content
with gr.Column(scale=8, elem_classes="main-content"):
# Analysis Section
with gr.Group(visible=True) as analysis_section:
# Upload 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"
)
# Analysis button
with gr.Row():
analyze_btn = gr.Button("πŸš€ Run Analysis", elem_classes="action-button", size="lg")
analysis_status = gr.Textbox(label="Analysis Status", interactive=False)
# Analysis Results Container (hidden initially)
with gr.Group(visible=False, elem_classes="analysis-results") as results_container:
# Key Metrics section
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)
# Performance Scores section
with gr.Group(elem_classes="section-card"):
gr.HTML('<div class="section-header">πŸ“ˆ Performance Scores</div>')
performance_plot = gr.Plot(label="Performance Chart")
# Performance Weights section
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")
# AI-Generated Summary section
with gr.Group(elem_classes="section-card"):
gr.HTML('<div class="section-header">πŸ€– AI-Generated Summary</div>')
ai_summary = gr.Markdown(label="Analysis Summary")
# Detailed Scores section
with gr.Group(elem_classes="section-card"):
gr.HTML('<div class="section-header">πŸ“‹ Detailed Scores</div>')
detailed_scores = gr.HTML(label="Supplier Performance Details")
# Audit Findings Summary section
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")
# Download Report section
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")
# Chat Section
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>')
# Chat interface
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="")
# Suggested questions
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
)
# Set up event handlers
api_key_input.change(
app.validate_api_key,
inputs=[api_key_input],
outputs=[api_status]
)
# Navigation handlers
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]
)
# Modified analyze button to show results container on success
def analyze_and_show_results(api_key, excel_file, audit_files):
result = app.process_files(api_key, excel_file, audit_files)
# Check if analysis was successful (status message starts with βœ…)
success = result[0].startswith("βœ…") if result[0] else False
return result + (gr.update(visible=success),) # Show results container if successful
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]
)
# Launch the interface
interface.launch(share=False)
if __name__ == "__main__":
main()