Spaces:
Runtime error
Runtime error
Download app.py from dhanush1601/sample: direct link, hf CLI and curl.
- Browser
- Download file 26.6 kB
-
https://huggingface.co/spaces/dhanush1601/sample/resolve/main/app.py
- Command line
-
hf download hf://spaces/dhanush1601/sample/app.py
-
curl -L -o app.py https://huggingface.co/spaces/dhanush1601/sample/resolve/main/app.py
26.6 kB
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import re | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| # ββ Page Configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.set_page_config( | |
| page_title="AI Ticket Classifier", | |
| page_icon="π§ ", | |
| layout="wide", | |
| initial_sidebar_state="collapsed" | |
| ) | |
| # ββ Custom CSS for Premium Dark Theme ββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); | |
| /* Base */ | |
| .stApp { | |
| background: linear-gradient(135deg, #0b0d17 0%, #111427 50%, #0d1020 100%); | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* Header */ | |
| .main-header { | |
| text-align: center; | |
| padding: 2rem 0 1rem; | |
| } | |
| .main-header h1 { | |
| font-size: 2.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, #818cf8, #a855f7, #06b6d4); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 0.3rem; | |
| } | |
| .main-header p { | |
| color: #9fa2b4; | |
| font-size: 1rem; | |
| } | |
| /* Stat Boxes */ | |
| .stat-container { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1.2rem; | |
| margin: 1rem 0 2rem; | |
| } | |
| .stat-box { | |
| background: rgba(17, 20, 39, 0.75); | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| border-radius: 12px; | |
| padding: 0.8rem 1.8rem; | |
| text-align: center; | |
| backdrop-filter: blur(10px); | |
| min-width: 140px; | |
| } | |
| .stat-box .stat-label { | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: #5e6278; | |
| } | |
| .stat-box .stat-value { | |
| font-size: 1.6rem; | |
| font-weight: 700; | |
| color: #818cf8; | |
| margin-top: 2px; | |
| } | |
| /* Cards */ | |
| .glass-card { | |
| background: rgba(17, 20, 39, 0.6); | |
| border: 1px solid rgba(99, 102, 241, 0.15); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| margin-bottom: 1.2rem; | |
| backdrop-filter: blur(12px); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| transition: border-color 0.3s ease; | |
| } | |
| .glass-card:hover { | |
| border-color: rgba(99, 102, 241, 0.35); | |
| } | |
| .card-title { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: #e8eaf6; | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| /* Category Badges */ | |
| .badge { | |
| display: inline-block; | |
| padding: 6px 18px; | |
| border-radius: 50px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: white; | |
| } | |
| .badge-password { background: linear-gradient(135deg, #6366f1, #818cf8); } | |
| .badge-hr { background: linear-gradient(135deg, #10b981, #34d399); } | |
| .badge-it { background: linear-gradient(135deg, #f59e0b, #fbbf24); } | |
| .badge-network { background: linear-gradient(135deg, #06b6d4, #22d3ee); } | |
| .badge-access { background: linear-gradient(135deg, #a855f7, #c084fc); } | |
| .badge-general { background: linear-gradient(135deg, #64748b, #94a3b8); } | |
| /* Confidence Bar */ | |
| .conf-bar-bg { | |
| width: 100%; | |
| height: 10px; | |
| background: rgba(255,255,255,0.06); | |
| border-radius: 5px; | |
| overflow: hidden; | |
| margin: 6px 0; | |
| } | |
| .conf-bar-fill { | |
| height: 100%; | |
| border-radius: 5px; | |
| background: linear-gradient(90deg, #6366f1, #10b981); | |
| transition: width 0.8s ease; | |
| } | |
| /* Response Box */ | |
| .response-box { | |
| padding: 1rem 1.2rem; | |
| background: rgba(16, 185, 129, 0.08); | |
| border: 1px solid rgba(16, 185, 129, 0.2); | |
| border-radius: 10px; | |
| color: #e8eaf6; | |
| font-size: 0.9rem; | |
| line-height: 1.7; | |
| } | |
| /* Keyword pills */ | |
| .kw-pill { | |
| display: inline-block; | |
| padding: 3px 14px; | |
| margin: 3px 4px; | |
| background: rgba(99, 102, 241, 0.12); | |
| border: 1px solid rgba(99, 102, 241, 0.25); | |
| border-radius: 50px; | |
| font-size: 0.78rem; | |
| font-weight: 500; | |
| color: #818cf8; | |
| } | |
| /* Ticket Item */ | |
| .ticket-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 14px; | |
| margin-bottom: 6px; | |
| border: 1px solid rgba(99, 102, 241, 0.12); | |
| border-radius: 8px; | |
| background: rgba(0,0,0,0.15); | |
| font-size: 0.88rem; | |
| color: #9fa2b4; | |
| } | |
| .ticket-num { | |
| font-size: 0.72rem; | |
| font-weight: 700; | |
| color: #5e6278; | |
| padding: 2px 8px; | |
| background: rgba(255,255,255,0.04); | |
| border-radius: 4px; | |
| flex-shrink: 0; | |
| } | |
| .ticket-conf { | |
| flex-shrink: 0; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| color: #10b981; | |
| } | |
| /* Group Header */ | |
| .group-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 8px; | |
| margin-top: 16px; | |
| } | |
| .group-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| } | |
| .group-name { | |
| font-size: 0.92rem; | |
| font-weight: 600; | |
| color: #e8eaf6; | |
| } | |
| .group-count { | |
| font-size: 0.72rem; | |
| padding: 2px 10px; | |
| border-radius: 50px; | |
| background: rgba(255,255,255,0.06); | |
| color: #5e6278; | |
| } | |
| /* Similarity heatmap */ | |
| .sim-cell { | |
| text-align: center; | |
| padding: 8px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| font-size: 0.8rem; | |
| } | |
| /* Hide Streamlit defaults */ | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| header {visibility: hidden;} | |
| .stDeployButton {display: none;} | |
| /* Button styling */ | |
| .stButton > button { | |
| background: linear-gradient(135deg, #6366f1, #a855f7) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 10px !important; | |
| padding: 0.6rem 1.5rem !important; | |
| font-weight: 600 !important; | |
| font-size: 0.9rem !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3) !important; | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 6px 28px rgba(99, 102, 241, 0.45) !important; | |
| } | |
| /* Text input */ | |
| .stTextArea textarea { | |
| background: rgba(0,0,0,0.25) !important; | |
| border: 1px solid rgba(99, 102, 241, 0.2) !important; | |
| border-radius: 10px !important; | |
| color: #e8eaf6 !important; | |
| font-family: 'Inter', sans-serif !important; | |
| } | |
| .stTextArea textarea:focus { | |
| border-color: #6366f1 !important; | |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25) !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ Knowledge Base βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CATEGORIES = { | |
| "password": { | |
| "name": "π Password / Authentication", | |
| "icon": "π", | |
| "css": "password", | |
| "color": "#6366f1", | |
| "keywords": [ | |
| "password", "passwd", "reset", "forgot", "login", "log in", "sign in", | |
| "signin", "authenticate", "credentials", "locked out", "lockout", | |
| "incorrect password", "wrong password", "cant log", "can't log", | |
| "unable to login", "access denied", "two factor", "2fa", "mfa", "otp" | |
| ], | |
| "response": ( | |
| "π **Password / Authentication Help**\n\n" | |
| "To reset your password:\n" | |
| "1. Go to the login page and click **Forgot Password**\n" | |
| "2. Enter your registered email address\n" | |
| "3. Check your inbox for the reset link (also check spam)\n" | |
| "4. Create a new password (min 8 chars, 1 uppercase, 1 number)\n\n" | |
| "If your account is locked, please wait 15 minutes or contact IT support at ext. 1234." | |
| ) | |
| }, | |
| "hr": { | |
| "name": "π HR / Leave Management", | |
| "icon": "π", | |
| "css": "hr", | |
| "color": "#10b981", | |
| "keywords": [ | |
| "leave", "leave balance", "vacation", "holiday", "sick leave", | |
| "casual leave", "attendance", "absence", "time off", "pto", | |
| "paid time off", "work from home", "wfh", "remote work", "salary", | |
| "payslip", "pay slip", "payroll", "bonus", "appraisal", "hr", | |
| "human resource", "benefits", "insurance", "medical", | |
| "bank account", "bank details", "compensation" | |
| ], | |
| "response": ( | |
| "π **HR / Leave Management Help**\n\n" | |
| "To check your leave balance:\n" | |
| "1. Log in to the HR Portal (hr.company.com)\n" | |
| "2. Navigate to **My Leave** β **Leave Balance**\n" | |
| "3. View category-wise balance (Casual, Sick, Earned)\n\n" | |
| "To apply for leave or WFH, go to My Leave β Apply Leave. " | |
| "For salary/payroll queries, contact hr@company.com." | |
| ) | |
| }, | |
| "it": { | |
| "name": "π» IT / Hardware Support", | |
| "icon": "π»", | |
| "css": "it", | |
| "color": "#f59e0b", | |
| "keywords": [ | |
| "laptop", "computer", "slow", "crash", "hang", "freeze", | |
| "blue screen", "bsod", "restart", "reboot", "software", "install", | |
| "update", "upgrade", "printer", "scanner", "monitor", "keyboard", | |
| "mouse", "hardware", "performance", "disk", "storage", "memory", "ram" | |
| ], | |
| "response": ( | |
| "π» **IT / Hardware Support Help**\n\n" | |
| "For hardware issues:\n" | |
| "1. Try restarting your device first\n" | |
| "2. Clear temporary files: Win+R β type 'temp' β delete all\n" | |
| "3. Check Task Manager (Ctrl+Shift+Esc) for high CPU/memory usage\n\n" | |
| "If the issue persists, raise a ticket at it-support.company.com or call ext. 5678." | |
| ) | |
| }, | |
| "network": { | |
| "name": "π Network / Connectivity", | |
| "icon": "π", | |
| "css": "network", | |
| "color": "#06b6d4", | |
| "keywords": [ | |
| "vpn", "network", "internet", "wifi", "wi-fi", "connect", | |
| "connection", "disconnect", "firewall", "proxy", "bandwidth", | |
| "speed", "slow internet", "office network", "remote access", | |
| "rdp", "remote desktop", "intranet", "server", "dns" | |
| ], | |
| "response": ( | |
| "π **Network / Connectivity Help**\n\n" | |
| "For VPN/network issues:\n" | |
| "1. Disconnect and reconnect your VPN client\n" | |
| "2. Ensure you're using the latest VPN client version\n" | |
| "3. Try switching between Wi-Fi and wired connection\n" | |
| "4. Restart your router/modem if working from home\n\n" | |
| "For persistent issues, contact Network Operations at ext. 9012." | |
| ) | |
| }, | |
| "access": { | |
| "name": "π Access / Permissions", | |
| "icon": "π", | |
| "css": "access", | |
| "color": "#a855f7", | |
| "keywords": [ | |
| "access", "permission", "role", "shared drive", "folder", | |
| "repository", "repo", "github", "gitlab", "jira", "confluence", | |
| "admin", "privileges", "unauthorized", "forbidden", "restricted", | |
| "grant access", "request access", "onboarding" | |
| ], | |
| "response": ( | |
| "π **Access / Permissions Help**\n\n" | |
| "To request access:\n" | |
| "1. Go to the Access Management Portal (access.company.com)\n" | |
| "2. Click **New Access Request**\n" | |
| "3. Search for the resource (shared drive, app, repo)\n" | |
| "4. Select the required role/permission level\n" | |
| "5. Submit β your manager will receive an approval request\n\n" | |
| "For urgent access needs, contact your IT admin directly." | |
| ) | |
| }, | |
| "general": { | |
| "name": "β General Inquiry", | |
| "icon": "β", | |
| "css": "general", | |
| "color": "#64748b", | |
| "keywords": [], | |
| "response": ( | |
| "β **General Inquiry**\n\n" | |
| "Thank you for reaching out! Your ticket has been received " | |
| "and will be reviewed by our support team.\n\n" | |
| "Expected response time: 2β4 business hours.\n\n" | |
| "For urgent issues, please call the helpdesk at ext. 1111." | |
| ) | |
| } | |
| } | |
| # ββ Stop Words βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| STOP_WORDS = { | |
| "i", "me", "my", "myself", "we", "our", "you", "your", "he", "she", | |
| "it", "its", "they", "them", "what", "which", "who", "this", "that", | |
| "am", "is", "are", "was", "were", "be", "been", "being", "have", "has", | |
| "had", "do", "does", "did", "a", "an", "the", "and", "but", "if", "or", | |
| "because", "as", "until", "while", "of", "at", "by", "for", "with", | |
| "about", "between", "through", "during", "before", "after", "to", "from", | |
| "up", "down", "in", "out", "on", "off", "over", "under", "again", | |
| "then", "once", "here", "there", "when", "where", "why", "how", "all", | |
| "both", "each", "few", "more", "most", "other", "some", "no", "not", | |
| "only", "own", "same", "so", "than", "too", "very", "can", "will", | |
| "just", "don", "should", "now", "please" | |
| } | |
| # ββ NLP Functions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def preprocess(text: str) -> str: | |
| """Lowercase, remove punctuation, remove stop words.""" | |
| text = text.lower() | |
| text = re.sub(r"[^a-z0-9\s'-]", " ", text) | |
| tokens = text.split() | |
| tokens = [t for t in tokens if t not in STOP_WORDS and len(t) > 1] | |
| return " ".join(tokens) | |
| def classify_ticket(text: str) -> dict: | |
| """Classify a ticket into a category using keyword matching.""" | |
| lower = text.lower() | |
| best_cat_id = "general" | |
| best_score = 0 | |
| matched_keywords = [] | |
| for cat_id, cat in CATEGORIES.items(): | |
| if not cat["keywords"]: | |
| continue | |
| score = 0 | |
| matched = [] | |
| for kw in cat["keywords"]: | |
| if kw in lower: | |
| weight = len(kw.split()) * 2 | |
| score += weight | |
| matched.append(kw) | |
| if score > best_score: | |
| best_score = score | |
| best_cat_id = cat_id | |
| matched_keywords = list(set(matched)) | |
| # Sigmoid-like confidence mapping | |
| confidence = 0.15 if best_score == 0 else min(0.99, 1 - 1 / (1 + best_score * 0.4)) | |
| return { | |
| "category_id": best_cat_id, | |
| "category": CATEGORIES[best_cat_id], | |
| "confidence": confidence, | |
| "matched_keywords": matched_keywords | |
| } | |
| def compute_similarity_matrix(texts: list) -> np.ndarray: | |
| """Compute pairwise cosine similarity using TF-IDF.""" | |
| if len(texts) < 2: | |
| return np.array([[1.0]]) | |
| processed = [preprocess(t) for t in texts] | |
| vectorizer = TfidfVectorizer() | |
| tfidf_matrix = vectorizer.fit_transform(processed) | |
| return cosine_similarity(tfidf_matrix) | |
| def sim_color(val: float) -> str: | |
| """Map similarity score to a background colour.""" | |
| if val > 0.8: | |
| return "rgba(16, 185, 129, 0.7)" | |
| if val > 0.6: | |
| return "rgba(16, 185, 129, 0.45)" | |
| if val > 0.4: | |
| return "rgba(99, 102, 241, 0.45)" | |
| if val > 0.2: | |
| return "rgba(99, 102, 241, 0.25)" | |
| return "rgba(255,255,255, 0.04)" | |
| # ββ Session State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if "tickets" not in st.session_state: | |
| # Pre-load the 3 example tickets | |
| examples = [ | |
| "I forgot my password, how to reset it?", | |
| "I can't log in, as password is incorrect", | |
| "How to see leave balance?" | |
| ] | |
| st.session_state.tickets = [] | |
| for i, text in enumerate(examples): | |
| result = classify_ticket(text) | |
| st.session_state.tickets.append({ | |
| "id": i + 1, | |
| "text": text, | |
| **result | |
| }) | |
| st.session_state.next_id = 4 | |
| # ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <div class="main-header"> | |
| <h1>π§ AI Ticket Classifier</h1> | |
| <p>Smart ticket grouping using TF-IDF & Cosine Similarity β powered by Python</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Stats | |
| tickets = st.session_state.tickets | |
| total = len(tickets) | |
| categories_used = len(set(t["category_id"] for t in tickets)) if tickets else 0 | |
| avg_conf = (sum(t["confidence"] for t in tickets) / total * 100) if total > 0 else 0 | |
| st.markdown(f""" | |
| <div class="stat-container"> | |
| <div class="stat-box"> | |
| <div class="stat-label">Total Tickets</div> | |
| <div class="stat-value">{total}</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Categories</div> | |
| <div class="stat-value">{categories_used}</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Avg Confidence</div> | |
| <div class="stat-value">{avg_conf:.0f}%</div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ Two-Column Layout βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| col_left, col_right = st.columns([1, 1], gap="large") | |
| # ββ LEFT COLUMN ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with col_left: | |
| # Submit Panel | |
| st.markdown('<div class="glass-card"><div class="card-title">βοΈ Submit a Ticket</div>', unsafe_allow_html=True) | |
| ticket_text = st.text_area( | |
| "Describe your issue", | |
| placeholder="e.g. I forgot my password, how to reset it?", | |
| height=100, | |
| label_visibility="collapsed" | |
| ) | |
| if st.button("β‘ Classify & Respond", use_container_width=True): | |
| if ticket_text.strip(): | |
| result = classify_ticket(ticket_text.strip()) | |
| new_ticket = { | |
| "id": st.session_state.next_id, | |
| "text": ticket_text.strip(), | |
| **result | |
| } | |
| st.session_state.tickets.append(new_ticket) | |
| st.session_state.next_id += 1 | |
| st.session_state.last_result = new_ticket | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Classification Result | |
| last = st.session_state.get("last_result") | |
| if last: | |
| cat = last["category"] | |
| pct = int(last["confidence"] * 100) | |
| st.markdown(f""" | |
| <div class="glass-card" style="border-color: rgba(99,102,241,0.4); box-shadow: 0 0 30px rgba(99,102,241,0.2);"> | |
| <div class="card-title">π§ Classification Result</div> | |
| <div style="margin-bottom:12px;"> | |
| <div style="font-size:0.7rem;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:#5e6278;margin-bottom:4px;">Category</div> | |
| <span class="badge badge-{cat['css']}">{cat['name']}</span> | |
| </div> | |
| <div style="margin-bottom:12px;"> | |
| <div style="font-size:0.7rem;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:#5e6278;margin-bottom:4px;">Confidence</div> | |
| <div class="conf-bar-bg"><div class="conf-bar-fill" style="width:{pct}%"></div></div> | |
| <span style="font-size:0.88rem;font-weight:700;color:#10b981;">{pct}%</span> | |
| </div> | |
| <div style="margin-bottom:12px;"> | |
| <div style="font-size:0.7rem;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:#5e6278;margin-bottom:4px;">Auto-Response</div> | |
| <div class="response-box">{cat['response'].replace(chr(10), '<br>')}</div> | |
| </div> | |
| <div> | |
| <div style="font-size:0.7rem;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:#5e6278;margin-bottom:4px;">Matched Keywords</div> | |
| {''.join(f'<span class="kw-pill">{kw}</span>' for kw in last['matched_keywords']) if last['matched_keywords'] else '<span class="kw-pill">No specific keywords matched</span>'} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Example Tickets | |
| st.markdown('<div class="glass-card"><div class="card-title">π‘ Try These Examples</div>', unsafe_allow_html=True) | |
| examples = [ | |
| "I forgot my password, how to reset it?", | |
| "I can't log in, as password is incorrect", | |
| "How to see leave balance?" | |
| ] | |
| for ex in examples: | |
| if st.button(ex, key=f"ex_{ex}", use_container_width=True): | |
| result = classify_ticket(ex) | |
| new_ticket = { | |
| "id": st.session_state.next_id, | |
| "text": ex, | |
| **result | |
| } | |
| st.session_state.tickets.append(new_ticket) | |
| st.session_state.next_id += 1 | |
| st.session_state.last_result = new_ticket | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # ββ RIGHT COLUMN βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with col_right: | |
| # Grouped Tickets | |
| st.markdown('<div class="glass-card"><div class="card-title">π Ticket Groups</div>', unsafe_allow_html=True) | |
| if not tickets: | |
| st.markdown(""" | |
| <div style="text-align:center;padding:40px 20px;color:#5e6278;"> | |
| <div style="font-size:2.5rem;margin-bottom:12px;">π</div> | |
| <p>No tickets submitted yet.<br/> Submit a ticket to see how they get grouped!</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| # Group by category | |
| groups = {} | |
| for t in tickets: | |
| cid = t["category_id"] | |
| if cid not in groups: | |
| groups[cid] = {"category": t["category"], "items": []} | |
| groups[cid]["items"].append(t) | |
| for cid, group in groups.items(): | |
| cat = group["category"] | |
| count = len(group["items"]) | |
| st.markdown(f""" | |
| <div class="group-header"> | |
| <span class="group-dot" style="background:{cat['color']};"></span> | |
| <span class="group-name">{cat['name']}</span> | |
| <span class="group-count">{count} ticket{'s' if count > 1 else ''}</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| for item in group["items"]: | |
| pct = int(item["confidence"] * 100) | |
| st.markdown(f""" | |
| <div class="ticket-row"> | |
| <span class="ticket-num">#{item['id']}</span> | |
| <span style="flex:1;">{item['text']}</span> | |
| <span class="ticket-conf">{pct}%</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if st.button("ποΈ Clear All", key="clear"): | |
| st.session_state.tickets = [] | |
| st.session_state.next_id = 1 | |
| st.session_state.pop("last_result", None) | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Similarity Matrix | |
| if len(tickets) >= 2: | |
| st.markdown('<div class="glass-card"><div class="card-title">π Similarity Matrix</div>', unsafe_allow_html=True) | |
| texts = [t["text"] for t in tickets] | |
| sim_matrix = compute_similarity_matrix(texts) | |
| # Build HTML table | |
| html = '<table style="width:100%;border-collapse:separate;border-spacing:3px;font-size:0.78rem;">' | |
| html += '<tr><th style="padding:6px;color:#5e6278;"></th>' | |
| for t in tickets: | |
| html += f'<th style="padding:6px;color:#5e6278;text-align:center;">#{t["id"]}</th>' | |
| html += '</tr>' | |
| for i, t in enumerate(tickets): | |
| html += f'<tr><th style="padding:6px;color:#5e6278;text-align:left;">#{t["id"]}</th>' | |
| for j in range(len(tickets)): | |
| val = sim_matrix[i][j] | |
| pct = int(val * 100) | |
| bg = sim_color(val) | |
| txt_color = "#fff" if val > 0.5 else "#c4c7d9" | |
| html += f'<td class="sim-cell" style="background:{bg};color:{txt_color};">{pct}%</td>' | |
| html += '</tr>' | |
| html += '</table>' | |
| st.markdown(html, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # ββ Footer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <div style="text-align:center;padding:24px;color:#5e6278;font-size:0.78rem;border-top:1px solid rgba(99,102,241,0.15);margin-top:2rem;"> | |
| AI Ticket Classifier β Python + Streamlit + scikit-learn (TF-IDF & Cosine Similarity) | |
| </div> | |
| """, unsafe_allow_html=True) | |