| import streamlit as st |
| import sys |
| import os |
| import json |
| from pathlib import Path |
| from dotenv import load_dotenv |
|
|
| |
| load_dotenv() |
|
|
| sys.path.append(os.path.dirname(os.path.dirname(__file__))) |
|
|
| PROCESSED_DIR = "data/processed_results" |
|
|
|
|
| def apply_material_css(): |
| st.markdown(""" |
| <style> |
| /* Material Design CSS Overrides */ |
| |
| .stApp { |
| font-family: 'Roboto', 'Inter', sans-serif; |
| background-color: #121212; |
| color: #FFFFFF; |
| } |
| |
| /* Material Cards for metrics and sections */ |
| .material-card { |
| background-color: #1E1E1E; |
| border-radius: 8px; |
| padding: 20px; |
| box-shadow: 0 4px 6px rgba(0,0,0,0.3); |
| margin-bottom: 20px; |
| } |
| |
| h1, h2, h3, h4 { |
| font-weight: 500; |
| } |
| |
| /* Subtle styling for Streamlit columns to look like cards */ |
| [data-testid="column"] { |
| background-color: #1E1E1E; |
| border-radius: 8px; |
| padding: 20px; |
| box-shadow: 0 4px 6px rgba(0,0,0,0.3); |
| } |
| |
| /* Expander (Transcript) - keep dark theme even on hover/focus */ |
| div[data-testid="stExpander"] details, |
| div[data-testid="stExpander"] summary, |
| div[data-testid="stExpander"] summary:hover, |
| div[data-testid="stExpander"] summary:focus, |
| div[data-testid="stExpander"] summary:active, |
| div[data-testid="stExpander"] summary:focus-visible { |
| background-color: #1E1E1E !important; |
| color: #FFFFFF !important; |
| } |
| |
| div[data-testid="stExpander"] details { |
| border: 1px solid #333333 !important; |
| border-radius: 8px !important; |
| } |
| |
| /* Transcript text area */ |
| div[data-testid="stExpander"] textarea, |
| div[data-testid="stExpander"] textarea:hover, |
| div[data-testid="stExpander"] textarea:focus, |
| div[data-testid="stExpander"] textarea:active { |
| background-color: #121212 !important; |
| color: #FFFFFF !important; |
| border-color: #333333 !important; |
| } |
| |
| </style> |
| """, unsafe_allow_html=True) |
|
|
|
|
| def display_results(final_state): |
| st.subheader("Workflow Results") |
|
|
| metadata = final_state.get("metadata", {}) |
| if isinstance(metadata, dict) and metadata.get("intake_error"): |
| st.error(f"CSV validation failed: {metadata['intake_error']}") |
| st.info("This CSV was not accepted for processing. Please fix the headers and re-upload.") |
| st.markdown("### Metadata") |
| for k, v in metadata.items(): |
| st.markdown(f"- **{k.replace('_', ' ').title()}**: {v}") |
| return |
|
|
| st.markdown("### Transcript") |
| transcript_text = final_state.get("clean_content") or final_state.get("content") or "" |
| if transcript_text: |
| with st.expander("View transcript", expanded=False): |
| st.text_area("Transcript", transcript_text, height=220) |
| else: |
| st.write("No transcript available.") |
|
|
| col1, col2 = st.columns(2) |
| with col1: |
| st.markdown("### Summary") |
| st.write(final_state.get("summary", "No summary generated.")) |
|
|
| st.markdown("### Key Points") |
| key_points = final_state.get("key_points", "No key points generated.") |
| if isinstance(key_points, list): |
| for point in key_points: |
| st.markdown(f"- {point}") |
| else: |
| st.write(key_points) |
|
|
| st.markdown("### Action Items") |
| action_items = final_state.get("action_items", "No action items generated.") |
| if isinstance(action_items, list): |
| if action_items: |
| for item in action_items: |
| st.markdown(f"- {item}") |
| else: |
| st.write("No action items generated.") |
| else: |
| st.write(action_items) |
|
|
| st.markdown("### Tags / Highlights") |
| tags = final_state.get("tags") or [] |
| highlights = final_state.get("highlights") or [] |
| if tags: |
| st.write("**Tags:** " + ", ".join([str(t) for t in tags])) |
| else: |
| st.write("**Tags:** None") |
| if highlights: |
| st.write("**Highlights:**") |
| for h in highlights: |
| st.markdown(f"- {h}") |
| else: |
| st.write("**Highlights:** None") |
|
|
| with col2: |
| st.markdown("### Scoring Rubric") |
| quality_scores = final_state.get("quality_scores", {}) |
| if isinstance(quality_scores, dict) and quality_scores: |
| import plotly.graph_objects as go |
| for metric in ['tone', 'professionalism', 'structured_resolution']: |
| if metric in quality_scores: |
| try: |
| val = float(quality_scores[metric]) |
| fig = go.Figure(go.Indicator( |
| mode="gauge+number", |
| value=val, |
| title={'text': metric.replace('_', ' ').title(), 'font': {'size': 16, 'color': '#FFFFFF'}}, |
| gauge={ |
| 'axis': {'range': [None, 10], 'tickwidth': 1, 'tickcolor': "#BB86FC"}, |
| 'bar': {'color': "#BB86FC"}, |
| 'bgcolor': "#1E1E1E", |
| 'borderwidth': 2, |
| 'bordercolor': "#333333", |
| 'steps': [ |
| {'range': [0, 4], 'color': '#cf6679'}, |
| {'range': [4, 7], 'color': '#ffb74d'}, |
| {'range': [7, 10], 'color': '#81c784'}], |
| } |
| )) |
| |
| fig.update_layout( |
| height=180, |
| margin=dict(l=20, r=20, t=40, b=20), |
| paper_bgcolor='#1E1E1E', |
| plot_bgcolor='#1E1E1E', |
| font={'color': '#FFFFFF'} |
| ) |
| st.plotly_chart(fig, width="stretch") |
| except (ValueError, TypeError): |
| st.write(f"**{metric.replace('_', ' ').title()}**: {quality_scores[metric]}") |
|
|
| if "notes" in quality_scores: |
| st.write("**Notes:**", quality_scores["notes"]) |
|
|
| if "rubric" in quality_scores and quality_scores["rubric"]: |
| with st.expander("View scoring rubric", expanded=False): |
| rubric_rows = [ |
| { |
| "Dimension": "Tone", |
| "0": "Hostile/arguing", |
| "3": "Curt/tense", |
| "5": "Neutral", |
| "7": "Friendly/empathic", |
| "10": "Consistently calm, respectful, de-escalating", |
| }, |
| { |
| "Dimension": "Professionalism", |
| "0": "Rude/unprofessional", |
| "3": "Unclear or dismissive", |
| "5": "Acceptable", |
| "7": "Clear, courteous, policy-aligned", |
| "10": "Excellent clarity, appropriate boundaries, ownership", |
| }, |
| { |
| "Dimension": "Structured resolution", |
| "0": "No attempt", |
| "3": "Vague / no next steps", |
| "5": "Partial (some questions/steps)", |
| "7": "Clear diagnosis + next steps + confirmation", |
| "10": "Fully structured (issue, actions, timelines, confirmation, closure)", |
| }, |
| ] |
|
|
| st.dataframe( |
| rubric_rows, |
| hide_index=True, |
| use_container_width=True, |
| ) |
| st.caption( |
| "Notes must cite 1–3 specific behaviors from the transcript (avoid long quotes)." |
| ) |
| else: |
| st.write("No scoring rubric results generated.", quality_scores) |
|
|
| st.markdown("### Metadata") |
| if isinstance(metadata, dict) and metadata: |
| for k, v in metadata.items(): |
| st.markdown(f"- **{k.replace('_', ' ').title()}**: {v}") |
| else: |
| st.write("No metadata available.") |
|
|
|
|
| def main(): |
| st.set_page_config(page_title="Call Center Data Analysis", layout="wide") |
| apply_material_css() |
|
|
| st.title("Call Center Data Analysis Dashboard") |
|
|
| os.makedirs(PROCESSED_DIR, exist_ok=True) |
| os.makedirs("tmp", exist_ok=True) |
|
|
| if "view_mode" not in st.session_state: |
| st.session_state.view_mode = "none" |
|
|
| def set_upload_mode(): |
| st.session_state.view_mode = "upload" |
|
|
| def set_dropdown_mode(): |
| st.session_state.view_mode = "dropdown" |
|
|
| st.sidebar.header("Upload New File") |
| uploaded_file = st.sidebar.file_uploader( |
| "Upload a file", |
| type=["mp3", "wav", "csv", "json"], |
| on_change=set_upload_mode |
| ) |
|
|
| st.sidebar.markdown("---") |
| st.sidebar.header("Processed Files") |
|
|
| |
| processed_files = [f for f in os.listdir(PROCESSED_DIR) if f.endswith(".json")] |
| processed_files.sort(reverse=True) |
|
|
| selected_file = None |
| if processed_files: |
| options = ["-- Select a file --"] + processed_files |
| selected_dropdown = st.sidebar.selectbox( |
| "View cached results:", |
| options, |
| format_func=lambda x: x.replace(".json", "") if x != "-- Select a file --" else x, |
| on_change=set_dropdown_mode |
| ) |
| if selected_dropdown != "-- Select a file --": |
| selected_file = selected_dropdown |
| else: |
| st.sidebar.info("No files processed yet.") |
|
|
| st.sidebar.markdown("---") |
| st.sidebar.header("Notes") |
| st.sidebar.markdown(""" |
| Sample data: |
| - [Customer Call Center Dataset Analysis](https://www.kaggle.com/datasets/rafaqatkhan608/customer-call-center-dataset-analysis/code/data) |
| - [E-commerce Customer Support English Audio](https://huggingface.co/datasets/HumynLabs/e-commerce-customersupport-english-audio/tree/main) |
| """) |
|
|
| |
| if st.session_state.view_mode == "upload" and uploaded_file is not None: |
| st.success(f"File '{uploaded_file.name}' uploaded successfully!") |
|
|
| from src.workflow import build_workflow |
|
|
| file_path = os.path.join("tmp", uploaded_file.name) |
| with open(file_path, "wb") as f: |
| f.write(uploaded_file.getbuffer()) |
|
|
| file_extension = uploaded_file.name.split('.')[-1].lower() |
|
|
| |
| cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json") |
|
|
| if os.path.exists(cached_json_path): |
| st.info("Loading cached results for this file...") |
| with open(cached_json_path, 'r') as f: |
| final_state = json.load(f) |
| display_results(final_state) |
| else: |
| st.write("Processing file through LangGraph Workflow...") |
| with st.spinner("Agents are analyzing the data..."): |
| workflow = build_workflow() |
| initial_state = { |
| "file_path": file_path, |
| "file_type": file_extension |
| } |
|
|
| thread_id = Path(file_path).name |
| final_state = workflow.invoke( |
| initial_state, config={"configurable": {"thread_id": thread_id}} |
| ) |
|
|
| if final_state.get("metadata", {}).get("intake_error"): |
| display_results(final_state) |
| else: |
| |
| with open(cached_json_path, 'w') as f: |
| json.dump(final_state, f) |
| st.rerun() |
|
|
| if not final_state.get("metadata", {}).get("intake_error"): |
| display_results(final_state) |
|
|
| elif ( |
| st.session_state.view_mode == "dropdown" or st.session_state.view_mode == "none") and selected_file is not None: |
| st.info(f"Loading cached results for '{selected_file.replace('.json', '')}'") |
| cached_json_path = os.path.join(PROCESSED_DIR, selected_file) |
| with open(cached_json_path, 'r') as f: |
| final_state = json.load(f) |
| display_results(final_state) |
|
|
| elif st.session_state.view_mode != "dropdown" and uploaded_file is not None: |
| st.success(f"File '{uploaded_file.name}' uploaded successfully!") |
|
|
| from src.workflow import build_workflow |
|
|
| file_path = os.path.join("tmp", uploaded_file.name) |
| with open(file_path, "wb") as f: |
| f.write(uploaded_file.getbuffer()) |
|
|
| file_extension = uploaded_file.name.split('.')[-1].lower() |
|
|
| cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json") |
|
|
| if os.path.exists(cached_json_path): |
| st.info("Loading cached results for this file...") |
| with open(cached_json_path, 'r') as f: |
| final_state = json.load(f) |
| display_results(final_state) |
| else: |
| st.write("Processing file through LangGraph Workflow...") |
| with st.spinner("Agents are analyzing the data..."): |
| workflow = build_workflow() |
| initial_state = { |
| "file_path": file_path, |
| "file_type": file_extension |
| } |
| thread_id = Path(file_path).name |
| final_state = workflow.invoke( |
| initial_state, config={"configurable": {"thread_id": thread_id}} |
| ) |
| if final_state.get("metadata", {}).get("intake_error"): |
| display_results(final_state) |
| else: |
| with open(cached_json_path, 'w') as f: |
| json.dump(final_state, f) |
| st.rerun() |
| if not final_state.get("metadata", {}).get("intake_error"): |
| display_results(final_state) |
|
|
| else: |
| st.info("Please upload a file or select a previously processed file.") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|