# utils.py import json import re import streamlit as st import pandas as pd import altair as alt from typing import List, Dict, Optional from config import DEFAULT_PERSONA_PATH, PERSONA_COLORS as CONFIG_PERSONA_COLORS import logging log = logging.getLogger(__name__) # local color cache seeded from config PERSONA_COLORS = dict(CONFIG_PERSONA_COLORS) if isinstance(CONFIG_PERSONA_COLORS, dict) else {} # ------------------------- # Personas I/O & validation # ------------------------- def load_personas_from_file(path: str = DEFAULT_PERSONA_PATH) -> List[Dict]: """Load personas.json from disk; return [] on any error.""" try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) if not isinstance(data, list): st.warning(f"⚠️ {path} content isn't a list. Returning empty list.") return [] return data except FileNotFoundError: # No file is OK — app will start with empty persona list log.info("personas file not found: %s", path) return [] except json.JSONDecodeError as e: st.error(f"❌ Malformed JSON in {path}: {e}") return [] except Exception as e: st.error(f"❌ Unexpected error loading {path}: {e}") return [] def get_personas(uploaded_file=None, path: str = DEFAULT_PERSONA_PATH) -> List[Dict]: """ Return personas list. If uploaded_file is supplied (Streamlit's UploadedFile), attempt to parse and replace saved personas. """ personas = load_personas_from_file(path) if uploaded_file: try: imported = json.load(uploaded_file) if not isinstance(imported, list): st.error("Uploaded file must contain a JSON list of personas.") else: personas = imported # persist to repo (Spaces runtime allows writing to repo workspace) try: with open(path, "w", encoding="utf-8") as f: json.dump(personas, f, indent=2) st.success("✅ Personas uploaded and saved.") except Exception as e: st.error(f"❌ Could not save uploaded personas: {e}") except json.JSONDecodeError: st.error("❌ Uploaded file contains invalid JSON.") except Exception as e: st.error(f"❌ Error reading uploaded file: {e}") return personas def validate_persona(persona: Dict) -> bool: required = ["name", "occupation", "tech_proficiency", "behavioral_traits"] for r in required: if r not in persona or persona[r] in (None, "", []): return False if not isinstance(persona.get("behavioral_traits", []), list): return False return True def save_personas(personas: List[Dict], path: str = DEFAULT_PERSONA_PATH) -> bool: try: with open(path, "w", encoding="utf-8") as f: json.dump(personas, f, indent=2) return True except Exception as e: st.error(f"❌ Could not save personas: {e}") log.exception("save_personas failed") return False # ------------------------- # Display & formatting # ------------------------- def get_color_for_persona(name: str) -> str: """Return stable hex color for persona.""" if name not in PERSONA_COLORS: PERSONA_COLORS[name] = f"#{(hash(name) & 0xFFFFFF):06x}" return PERSONA_COLORS[name] def format_response_line(text: str, persona_name: str, highlight: Optional[str] = None) -> str: """Return small styled HTML block for a persona line.""" color = get_color_for_persona(persona_name) background = "" if highlight == "insight": background = "background-color: #d4edda;" elif highlight == "concern": background = "background-color: #f8d7da;" return ( f"