sample / app.py
dhanush1601's picture
Upload 3 files
5fd1a13 verified
Raw History Blame Contribute Delete
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)