bitspiresolutions commited on
Commit
16cd190
·
1 Parent(s): b2cea0d
Files changed (8) hide show
  1. .env +1 -0
  2. .gitignore +2 -0
  3. Dockerfile +43 -0
  4. README.md +3 -3
  5. app.py +617 -0
  6. data/synthetic_knowledge_items.csv +0 -0
  7. requirements.txt +11 -0
  8. templates/index.html +1160 -0
.env ADDED
@@ -0,0 +1 @@
 
 
1
+ GROQ_API_KEY = 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
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ venv
2
+ *__pycache__
Dockerfile ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for Hugging Face Spaces
2
+ # Uses Python 3.9 with minimal dependencies
3
+
4
+ FROM python:3.9-slim
5
+
6
+ # Set working directory
7
+ WORKDIR /app
8
+
9
+ # Install system dependencies
10
+ RUN apt-get update && apt-get install -y \
11
+ gcc \
12
+ g++ \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy requirements first for better caching
16
+ COPY requirements.txt .
17
+
18
+ # Install Python dependencies
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Copy application files
22
+ COPY app.py .
23
+ COPY templates/ ./templates/
24
+ COPY data/ ./data/
25
+
26
+ # Create necessary directories
27
+ RUN mkdir -p static vector_store
28
+
29
+ # Set environment variables for Hugging Face
30
+ ENV HF_SPACE=True
31
+ ENV PORT=7860
32
+ ENV PYTHONUNBUFFERED=1
33
+ ENV PYTHONPATH=/app
34
+
35
+ # Expose port
36
+ EXPOSE 7860
37
+
38
+ # Health check
39
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
40
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/api/health')"
41
+
42
+ # Run the application
43
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
  title: Itsm Knowledge Chatbot
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
 
1
  ---
2
  title: Itsm Knowledge Chatbot
3
+ emoji: 👁
4
+ colorFrom: indigo
5
+ colorTo: red
6
  sdk: docker
7
  pinned: false
8
  ---
app.py ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ITSM Knowledge Base Chatbot - Hugging Face Deployment
3
+ Optimized for Spaces with minimal dependencies
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+ import faiss
10
+ import pickle
11
+ import numpy as np
12
+ import pandas as pd
13
+ import uuid
14
+ from datetime import datetime
15
+ from typing import List, Dict, Any, Optional
16
+ from dataclasses import dataclass, asdict
17
+ import threading
18
+
19
+ from flask import Flask, request, jsonify, render_template, session
20
+ from sentence_transformers import SentenceTransformer
21
+ from dotenv import load_dotenv
22
+
23
+ # Try to import Groq (optional)
24
+ try:
25
+ from groq import Groq
26
+ GROQ_AVAILABLE = True
27
+ except ImportError:
28
+ GROQ_AVAILABLE = False
29
+ print("⚠️ Groq not installed. Using simple responses.")
30
+
31
+ # ==================== CONFIGURATION ====================
32
+ load_dotenv()
33
+
34
+ class Config:
35
+ EMBEDDING_MODEL = "all-MiniLM-L6-v2"
36
+ CSV_PATH = "data/synthetic_knowledge_items.csv"
37
+ VECTOR_STORE_DIR = "vector_store"
38
+ INDEX_PATH = f"{VECTOR_STORE_DIR}/kb_index.faiss"
39
+ METADATA_PATH = f"{VECTOR_STORE_DIR}/kb_metadata.pkl"
40
+ GROQ_MODEL = "llama3-70b-8192"
41
+ HISTORY_FILE = "chat_data.json"
42
+ SECRET_KEY = os.getenv("SECRET_KEY", "dev-key-for-huggingface")
43
+ HF_SPACE = os.getenv("HF_SPACE", "False") == "True"
44
+
45
+ # ==================== DATACLASSES ====================
46
+ @dataclass
47
+ class KnowledgeItem:
48
+ id: str
49
+ topic: str
50
+ text: str
51
+ alt_text: str = ""
52
+ bad_text: str = ""
53
+ category: str = "General"
54
+
55
+ @dataclass
56
+ class SupportTicket:
57
+ id: str
58
+ description: str
59
+ status: str
60
+ priority: str
61
+ category: str
62
+ created_at: str
63
+ created_by: str
64
+ estimated_resolution: str = ""
65
+
66
+ # ==================== INDEX BUILDER ====================
67
+ class KnowledgeIndexBuilder:
68
+ def __init__(self):
69
+ # Use smaller model for Hugging Face
70
+ self.model = SentenceTransformer(Config.EMBEDDING_MODEL)
71
+ self.index = None
72
+ self.knowledge_items = []
73
+
74
+ def build_index(self):
75
+ """Build FAISS index from CSV file"""
76
+ print(f"📚 Building index from: {Config.CSV_PATH}")
77
+
78
+ if not os.path.exists(Config.CSV_PATH):
79
+ raise FileNotFoundError(f"CSV file not found: {Config.CSV_PATH}")
80
+
81
+ # Load and process CSV
82
+ df = pd.read_csv(Config.CSV_PATH)
83
+ print(f"📊 Loaded {len(df)} rows")
84
+
85
+ processed_items = []
86
+ texts_for_embedding = []
87
+
88
+ # Process in smaller batches for Hugging Face
89
+ for idx, row in df.iterrows():
90
+ if idx >= 100 and Config.HF_SPACE: # Limit for Hugging Face
91
+ break
92
+
93
+ # Clean data
94
+ topic = str(row.get('ki_topic', '')).strip()
95
+ text = str(row.get('ki_text', '')).strip()
96
+ alt_text = str(row.get('alt_ki_text', '')).strip()
97
+ bad_text = str(row.get('bad_ki_text', '')).strip()
98
+
99
+ if not topic and not text:
100
+ continue
101
+
102
+ # Create knowledge item
103
+ item_id = f"kb_{idx:04d}"
104
+ category = self._extract_category(topic)
105
+
106
+ item = KnowledgeItem(
107
+ id=item_id,
108
+ topic=topic,
109
+ text=text,
110
+ alt_text=alt_text,
111
+ bad_text=bad_text,
112
+ category=category
113
+ )
114
+
115
+ # For embedding
116
+ embedding_text = f"{topic}. {text}"
117
+ processed_items.append(item)
118
+ texts_for_embedding.append(embedding_text)
119
+
120
+ print(f"✅ Processed {len(processed_items)} unique items")
121
+
122
+ if not processed_items:
123
+ raise ValueError("No valid knowledge items found")
124
+
125
+ # Generate embeddings
126
+ print("🔢 Generating embeddings...")
127
+ embeddings = self.model.encode(
128
+ texts_for_embedding,
129
+ convert_to_numpy=True,
130
+ normalize_embeddings=True,
131
+ show_progress_bar=True
132
+ )
133
+
134
+ # Create FAISS index
135
+ dim = embeddings.shape[1]
136
+ self.index = faiss.IndexFlatIP(dim)
137
+ self.index.add(embeddings.astype('float32'))
138
+
139
+ self.knowledge_items = processed_items
140
+
141
+ # Save to disk
142
+ self._save_to_disk()
143
+
144
+ # Print statistics
145
+ self._print_statistics()
146
+
147
+ return self
148
+
149
+ def _extract_category(self, topic: str) -> str:
150
+ """Extract category from topic"""
151
+ categories = {
152
+ 'password': 'Authentication', 'login': 'Authentication',
153
+ 'vpn': 'Network', 'wifi': 'Network',
154
+ 'email': 'Communication', 'outlook': 'Communication',
155
+ 'software': 'Software', 'install': 'Software',
156
+ 'printer': 'Hardware', 'hardware': 'Hardware'
157
+ }
158
+
159
+ topic_lower = topic.lower()
160
+ for keyword, category in categories.items():
161
+ if keyword in topic_lower:
162
+ return category
163
+ return 'General'
164
+
165
+ def _save_to_disk(self):
166
+ """Save index and metadata to disk"""
167
+ os.makedirs(Config.VECTOR_STORE_DIR, exist_ok=True)
168
+
169
+ # Save FAISS index
170
+ faiss.write_index(self.index, Config.INDEX_PATH)
171
+ print(f"💾 Saved FAISS index to: {Config.INDEX_PATH}")
172
+
173
+ # Save metadata
174
+ metadata = []
175
+ for item in self.knowledge_items:
176
+ metadata.append(asdict(item))
177
+
178
+ with open(Config.METADATA_PATH, 'wb') as f:
179
+ pickle.dump(metadata, f)
180
+ print(f"💾 Saved metadata to: {Config.METADATA_PATH}")
181
+
182
+ # Save statistics
183
+ stats = {
184
+ 'total_items': len(self.knowledge_items),
185
+ 'categories': {},
186
+ 'created_at': datetime.now().isoformat(),
187
+ 'deployment': 'huggingface' if Config.HF_SPACE else 'local'
188
+ }
189
+
190
+ for item in self.knowledge_items:
191
+ stats['categories'][item.category] = stats['categories'].get(item.category, 0) + 1
192
+
193
+ stats_path = f"{Config.VECTOR_STORE_DIR}/stats.json"
194
+ with open(stats_path, 'w') as f:
195
+ json.dump(stats, f, indent=2)
196
+ print(f"📊 Saved statistics to: {stats_path}")
197
+
198
+ def _print_statistics(self):
199
+ """Print index statistics"""
200
+ print(f"\n📊 Index Statistics:")
201
+ print(f" Total knowledge items: {len(self.knowledge_items)}")
202
+ print(f" Embedding dimension: {self.index.d}")
203
+ print(f" Index size: {self.index.ntotal} vectors")
204
+ print(f" Deployment: {'Hugging Face' if Config.HF_SPACE else 'Local'}")
205
+
206
+ # ==================== KNOWLEDGE RETRIEVER ====================
207
+ class KnowledgeRetriever:
208
+ def __init__(self):
209
+ """Load existing index or build if not exists"""
210
+ self.model = SentenceTransformer(Config.EMBEDDING_MODEL)
211
+ self.index = None
212
+ self.knowledge_data = []
213
+ self._load_index()
214
+
215
+ def _load_index(self):
216
+ """Load FAISS index and metadata"""
217
+ if not os.path.exists(Config.INDEX_PATH):
218
+ print("⚠️ Index not found. Building now...")
219
+ builder = KnowledgeIndexBuilder()
220
+ builder.build_index()
221
+
222
+ # Load index
223
+ self.index = faiss.read_index(Config.INDEX_PATH)
224
+
225
+ # Load metadata
226
+ with open(Config.METADATA_PATH, 'rb') as f:
227
+ self.knowledge_data = pickle.load(f)
228
+
229
+ print(f"✅ Loaded {len(self.knowledge_data)} knowledge items")
230
+
231
+ def retrieve(self, query: str, top_k: int = 3, similarity_threshold: float = 0.3) -> List[Dict]:
232
+ """Retrieve relevant knowledge items (optimized for Hugging Face)"""
233
+ # Encode query
234
+ query_embedding = self.model.encode([query], convert_to_numpy=True)
235
+ query_embedding = query_embedding.astype('float32')
236
+
237
+ # Normalize for cosine similarity
238
+ faiss.normalize_L2(query_embedding)
239
+
240
+ # Search
241
+ k = min(top_k * 2, len(self.knowledge_data))
242
+ scores, indices = self.index.search(query_embedding, k)
243
+
244
+ # Process results
245
+ results = []
246
+ for score, idx in zip(scores[0], indices[0]):
247
+ if idx < 0 or idx >= len(self.knowledge_data):
248
+ continue
249
+
250
+ similarity = float(score)
251
+ if similarity >= similarity_threshold:
252
+ item = self.knowledge_data[idx].copy()
253
+ item['similarity'] = similarity
254
+ results.append(item)
255
+
256
+ # Sort by similarity
257
+ results.sort(key=lambda x: x['similarity'], reverse=True)
258
+ return results[:top_k]
259
+
260
+ # ==================== CHAT ENGINE ====================
261
+ class ChatEngine:
262
+ def __init__(self):
263
+ self.retriever = KnowledgeRetriever()
264
+ self.groq_client = None
265
+ self.active_tickets = {}
266
+ self._init_groq()
267
+
268
+ def _init_groq(self):
269
+ """Initialize Groq client if available"""
270
+ if GROQ_AVAILABLE:
271
+ api_key = os.getenv("GROQ_API_KEY")
272
+ if api_key:
273
+ try:
274
+ self.groq_client = Groq(api_key=api_key)
275
+ print(f"✅ Groq integration enabled (Model: {Config.GROQ_MODEL})")
276
+ except Exception as e:
277
+ print(f"⚠️ Groq initialization failed: {e}")
278
+ self.groq_client = None
279
+ else:
280
+ print("⚠️ GROQ_API_KEY not found. Using simple responses.")
281
+ else:
282
+ print("ℹ️ Using simple response mode")
283
+
284
+ def generate_response(self, query: str, context: str) -> str:
285
+ """Generate response using Groq or fallback"""
286
+ if self.groq_client:
287
+ try:
288
+ return self._generate_groq_response(query, context)
289
+ except Exception as e:
290
+ print(f"⚠️ Groq error: {e}")
291
+
292
+ # Fallback response
293
+ return self._generate_fallback_response(query, context)
294
+
295
+ def _generate_groq_response(self, query: str, context: str) -> str:
296
+ """Generate response using Groq API"""
297
+ system_prompt = """You are a helpful IT support assistant.
298
+ Provide clear, step-by-step instructions based on the company knowledge base.
299
+ Be concise but thorough. Use bullet points when helpful."""
300
+
301
+ user_prompt = f"""User Query: {query}
302
+
303
+ Relevant Knowledge Base Information:
304
+ {context}
305
+
306
+ Based on the above knowledge base information, provide a helpful response."""
307
+
308
+ try:
309
+ response = self.groq_client.chat.completions.create(
310
+ model=Config.GROQ_MODEL,
311
+ messages=[
312
+ {"role": "system", "content": system_prompt},
313
+ {"role": "user", "content": user_prompt}
314
+ ],
315
+ temperature=0.3,
316
+ max_tokens=300, # Reduced for Hugging Face
317
+ timeout=30 # Add timeout for Hugging Face
318
+ )
319
+
320
+ return response.choices[0].message.content.strip()
321
+ except Exception as e:
322
+ print(f"Groq API call failed: {e}")
323
+ return self._generate_fallback_response(query, context)
324
+
325
+ def _generate_fallback_response(self, query: str, context: str) -> str:
326
+ """Generate simple response without AI"""
327
+ if "No relevant knowledge found" in context:
328
+ return "I couldn't find specific information about that in our knowledge base. Could you please provide more details?"
329
+
330
+ # Try to extract useful information
331
+ lines = context.split('\n')
332
+ for i, line in enumerate(lines):
333
+ if "text:" in line.lower() and i + 1 < len(lines):
334
+ return f"Based on our knowledge base:\n\n{lines[i+1].strip()}"
335
+
336
+ return "I found some relevant information. Please check with IT support for detailed assistance."
337
+
338
+ def process_query(self, query: str, user_id: str = "anonymous") -> Dict[str, Any]:
339
+ """Process user query and generate response"""
340
+ # Handle greetings
341
+ query_lower = query.lower()
342
+ if any(word in query_lower for word in ["hello", "hi", "hey", "greetings"]):
343
+ return {
344
+ "answer": "Hello! I'm your ITSM assistant. I can help you with IT issues, knowledge base searches, and ticket creation. How can I assist you today?",
345
+ "timestamp": datetime.now().isoformat(),
346
+ "sources": [],
347
+ "intent": "greeting"
348
+ }
349
+
350
+ # Retrieve relevant knowledge
351
+ results = self.retriever.retrieve(query, top_k=2) # Reduced for Hugging Face
352
+
353
+ # Prepare context
354
+ if results:
355
+ context_parts = []
356
+ for i, item in enumerate(results, 1):
357
+ context = f"[{i}] {item['topic']}\n"
358
+ context += f"Category: {item['category']}\n"
359
+ context += f"Instruction: {item['text']}\n"
360
+ context_parts.append(context)
361
+ context = "\n".join(context_parts)
362
+ else:
363
+ context = "No relevant knowledge found."
364
+
365
+ # Generate response
366
+ answer = self.generate_response(query, context)
367
+
368
+ # Prepare response
369
+ response = {
370
+ "answer": answer,
371
+ "timestamp": datetime.now().isoformat(),
372
+ "sources": [],
373
+ "context_used": bool(results)
374
+ }
375
+
376
+ # Add sources if available
377
+ if results:
378
+ response["sources"] = [{
379
+ "topic": r["topic"],
380
+ "category": r["category"],
381
+ "confidence": f"{r.get('similarity', 0):.0%}"
382
+ } for r in results]
383
+
384
+ return response
385
+
386
+ # ==================== CHAT HISTORY MANAGER ====================
387
+ class ChatHistoryManager:
388
+ def __init__(self):
389
+ self.history_file = Config.HISTORY_FILE
390
+ self.lock = threading.RLock()
391
+ self._ensure_file()
392
+
393
+ def _ensure_file(self):
394
+ """Ensure history file exists"""
395
+ if not os.path.exists(self.history_file):
396
+ with open(self.history_file, 'w') as f:
397
+ json.dump({"sessions": {}}, f)
398
+
399
+ def add_message(self, session_id: str, role: str, content: str):
400
+ """Add message to chat history (simplified for Hugging Face)"""
401
+ with self.lock:
402
+ try:
403
+ # Load existing data
404
+ with open(self.history_file, 'r') as f:
405
+ data = json.load(f)
406
+
407
+ # Ensure session exists
408
+ if 'sessions' not in data:
409
+ data['sessions'] = {}
410
+
411
+ if session_id not in data['sessions']:
412
+ data['sessions'][session_id] = {
413
+ "created_at": datetime.now().isoformat(),
414
+ "messages": []
415
+ }
416
+
417
+ # Add message (limit to last 10 messages per session for Hugging Face)
418
+ message = {
419
+ "role": role,
420
+ "content": content,
421
+ "timestamp": datetime.now().isoformat()
422
+ }
423
+
424
+ data['sessions'][session_id]['messages'].append(message)
425
+
426
+ # Keep only last 10 messages
427
+ if len(data['sessions'][session_id]['messages']) > 10:
428
+ data['sessions'][session_id]['messages'] = data['sessions'][session_id]['messages'][-10:]
429
+
430
+ data['sessions'][session_id]['last_activity'] = datetime.now().isoformat()
431
+
432
+ # Save back
433
+ with open(self.history_file, 'w') as f:
434
+ json.dump(data, f)
435
+ except Exception as e:
436
+ print(f"Error saving chat history: {e}")
437
+
438
+ def get_messages(self, session_id: str) -> List[Dict]:
439
+ """Get messages from a session"""
440
+ with self.lock:
441
+ try:
442
+ with open(self.history_file, 'r') as f:
443
+ data = json.load(f)
444
+
445
+ if 'sessions' not in data or session_id not in data['sessions']:
446
+ return []
447
+
448
+ return data['sessions'][session_id].get('messages', [])
449
+ except Exception as e:
450
+ print(f"Error loading chat history: {e}")
451
+ return []
452
+
453
+ # ==================== FLASK APPLICATION ====================
454
+ app = Flask(__name__,
455
+ static_folder='static',
456
+ template_folder='templates')
457
+ app.secret_key = Config.SECRET_KEY
458
+
459
+ # Initialize components
460
+ chat_engine = ChatEngine()
461
+ history_manager = ChatHistoryManager()
462
+
463
+ # ==================== ROUTES ====================
464
+ @app.route('/')
465
+ def home():
466
+ """Serve the main chat interface"""
467
+ # Generate session ID if not exists
468
+ if 'session_id' not in session:
469
+ session['session_id'] = str(uuid.uuid4())[:8]
470
+
471
+ return render_template('index.html',
472
+ session_id=session['session_id'],
473
+ hf_space=Config.HF_SPACE)
474
+
475
+ @app.route('/api/chat', methods=['POST'])
476
+ def chat():
477
+ """Handle chat messages"""
478
+ try:
479
+ data = request.json
480
+ user_message = data.get('message', '').strip()
481
+ session_id = data.get('session_id') or session.get('session_id')
482
+
483
+ if not user_message:
484
+ return jsonify({'error': 'Message is required'}), 400
485
+
486
+ if not session_id:
487
+ session_id = str(uuid.uuid4())[:8]
488
+ session['session_id'] = session_id
489
+
490
+ # Save user message
491
+ history_manager.add_message(session_id, 'user', user_message)
492
+
493
+ # Process through chat engine
494
+ response = chat_engine.process_query(user_message, session_id)
495
+
496
+ # Save bot response
497
+ history_manager.add_message(session_id, 'assistant', response['answer'])
498
+
499
+ # Return response
500
+ return jsonify({
501
+ 'success': True,
502
+ 'session_id': session_id,
503
+ 'response': response['answer'],
504
+ 'sources': response.get('sources', []),
505
+ 'timestamp': response['timestamp']
506
+ })
507
+
508
+ except Exception as e:
509
+ print(f"Error in chat endpoint: {e}")
510
+ return jsonify({
511
+ 'success': False,
512
+ 'error': 'Internal server error',
513
+ 'response': 'Sorry, I encountered an error. Please try again.'
514
+ }), 500
515
+
516
+ @app.route('/api/history/<session_id>', methods=['GET'])
517
+ def get_history(session_id):
518
+ """Get chat history for a session"""
519
+ try:
520
+ messages = history_manager.get_messages(session_id)
521
+ return jsonify({
522
+ 'success': True,
523
+ 'session_id': session_id,
524
+ 'messages': messages,
525
+ 'count': len(messages)
526
+ })
527
+ except Exception as e:
528
+ return jsonify({'success': False, 'error': str(e)}), 500
529
+
530
+ @app.route('/api/health', methods=['GET'])
531
+ def health_check():
532
+ """Health check endpoint"""
533
+ return jsonify({
534
+ 'status': 'healthy',
535
+ 'timestamp': datetime.now().isoformat(),
536
+ 'deployment': 'huggingface' if Config.HF_SPACE else 'local',
537
+ 'knowledge_items': len(chat_engine.retriever.knowledge_data),
538
+ 'groq_available': GROQ_AVAILABLE and bool(os.getenv("GROQ_API_KEY")),
539
+ 'version': '1.0.0'
540
+ })
541
+
542
+ @app.route('/api/clear', methods=['POST'])
543
+ def clear_session():
544
+ """Clear current session"""
545
+ try:
546
+ session_id = request.json.get('session_id') or session.get('session_id')
547
+ if 'session_id' in session:
548
+ session.pop('session_id')
549
+
550
+ return jsonify({
551
+ 'success': True,
552
+ 'message': 'Session cleared'
553
+ })
554
+ except Exception as e:
555
+ return jsonify({'success': False, 'error': str(e)}), 500
556
+
557
+ # ==================== UTILITY FUNCTIONS ====================
558
+ def ensure_directories():
559
+ """Ensure required directories exist"""
560
+ os.makedirs('data', exist_ok=True)
561
+ os.makedirs('vector_store', exist_ok=True)
562
+ os.makedirs('static', exist_ok=True)
563
+ os.makedirs('templates', exist_ok=True)
564
+
565
+ # Check for CSV file
566
+ if not os.path.exists(Config.CSV_PATH):
567
+ print(f"⚠️ Warning: CSV file not found at {Config.CSV_PATH}")
568
+ # Create a minimal sample CSV for Hugging Face demo
569
+ sample_data = pd.DataFrame({
570
+ 'ki_topic': [
571
+ 'Password Reset Guide',
572
+ 'VPN Connection Issues',
573
+ 'Email Setup Instructions'
574
+ ],
575
+ 'ki_text': [
576
+ 'To reset your password, visit the company portal and click "Forgot Password".',
577
+ 'For VPN issues, check your internet connection and restart the VPN client.',
578
+ 'Configure email by entering server settings: mail.company.com, port 993.'
579
+ ],
580
+ 'alt_ki_text': ['', '', ''],
581
+ 'bad_ki_text': ['', '', '']
582
+ })
583
+ sample_data.to_csv(Config.CSV_PATH, index=False)
584
+ print(f"📁 Created sample CSV at {Config.CSV_PATH}")
585
+
586
+ return True
587
+
588
+ # ==================== MAIN ENTRY ====================
589
+ if __name__ == '__main__':
590
+ # Ensure directories exist
591
+ ensure_directories()
592
+
593
+ # Check if index needs to be built
594
+ if not os.path.exists(Config.INDEX_PATH):
595
+ print("🔨 Building knowledge index...")
596
+ try:
597
+ builder = KnowledgeIndexBuilder()
598
+ builder.build_index()
599
+ print("✅ Index built successfully!")
600
+ except Exception as e:
601
+ print(f"❌ Error building index: {e}")
602
+
603
+ # Print startup info
604
+ print("\n" + "="*60)
605
+ print("🚀 ITSM Knowledge Base Chatbot")
606
+ print("="*60)
607
+ print(f"📚 Knowledge Base: {len(chat_engine.retriever.knowledge_data)} items")
608
+ print(f"🤖 Chat Engine: Ready")
609
+ print(f"🌐 Deployment: {'Hugging Face Space' if Config.HF_SPACE else 'Local'}")
610
+ print(f"🔗 URL: http://localhost:5000")
611
+ print("="*60 + "\n")
612
+
613
+ # Run the application
614
+ port = int(os.getenv("PORT", 5000))
615
+ debug = not Config.HF_SPACE # Disable debug in production/HF
616
+
617
+ app.run(debug=debug, host='0.0.0.0', port=port)
data/synthetic_knowledge_items.csv ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ selenium
3
+ groq
4
+ langchain
5
+ langchain-community
6
+ langchain_groq
7
+ faiss-cpu
8
+ sentence-transformers
9
+ langchain_huggingface
10
+ flask
11
+ flask[async]
templates/index.html ADDED
@@ -0,0 +1,1160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ITSM Knowledge Base Chatbot 🤖</title>
7
+
8
+ <!-- Bootstrap 5 CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Bootstrap Icons -->
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
13
+
14
+ <!-- Custom CSS -->
15
+ <style>
16
+ :root {
17
+ --primary-color: #4361ee;
18
+ --secondary-color: #3a0ca3;
19
+ --success-color: #4cc9f0;
20
+ --light-color: #f8f9fa;
21
+ --dark-color: #212529;
22
+ }
23
+
24
+ body {
25
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
26
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
27
+ min-height: 100vh;
28
+ }
29
+
30
+ .chat-container {
31
+ max-width: 1400px;
32
+ margin: 20px auto;
33
+ background: white;
34
+ border-radius: 20px;
35
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
36
+ overflow: hidden;
37
+ height: 95vh;
38
+ }
39
+
40
+ /* Header Styles */
41
+ .chat-header {
42
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
43
+ color: white;
44
+ padding: 1.5rem;
45
+ }
46
+
47
+ .chat-header h1 {
48
+ font-weight: 600;
49
+ margin: 0;
50
+ }
51
+
52
+ .status-indicator {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 8px;
56
+ background: rgba(255,255,255,0.2);
57
+ padding: 6px 12px;
58
+ border-radius: 20px;
59
+ font-size: 0.9rem;
60
+ }
61
+
62
+ .status-dot {
63
+ width: 10px;
64
+ height: 10px;
65
+ border-radius: 50%;
66
+ background: #4caf50;
67
+ animation: pulse 2s infinite;
68
+ }
69
+
70
+ @keyframes pulse {
71
+ 0% { opacity: 1; }
72
+ 50% { opacity: 0.5; }
73
+ 100% { opacity: 1; }
74
+ }
75
+
76
+ /* Main Layout */
77
+ .chat-body {
78
+ display: flex;
79
+ height: calc(100% - 120px);
80
+ }
81
+
82
+ .sidebar {
83
+ width: 300px;
84
+ background: #f8f9fa;
85
+ border-right: 1px solid #dee2e6;
86
+ padding: 1.5rem;
87
+ overflow-y: auto;
88
+ }
89
+
90
+ .chat-area {
91
+ flex: 1;
92
+ display: flex;
93
+ flex-direction: column;
94
+ padding: 0;
95
+ }
96
+
97
+ /* Messages Area */
98
+ .messages-container {
99
+ flex: 1;
100
+ overflow-y: auto;
101
+ padding: 1.5rem;
102
+ background: #f8f9fa;
103
+ }
104
+
105
+ .message {
106
+ margin-bottom: 1.5rem;
107
+ animation: slideIn 0.3s ease-out;
108
+ }
109
+
110
+ @keyframes slideIn {
111
+ from {
112
+ opacity: 0;
113
+ transform: translateY(10px);
114
+ }
115
+ to {
116
+ opacity: 1;
117
+ transform: translateY(0);
118
+ }
119
+ }
120
+
121
+ .message.user {
122
+ margin-left: auto;
123
+ max-width: 70%;
124
+ }
125
+
126
+ .message.assistant, .message.system {
127
+ margin-right: auto;
128
+ max-width: 70%;
129
+ }
130
+
131
+ .message-bubble {
132
+ padding: 1rem 1.25rem;
133
+ border-radius: 18px;
134
+ position: relative;
135
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
136
+ }
137
+
138
+ .message.user .message-bubble {
139
+ background: var(--primary-color);
140
+ color: white;
141
+ border-bottom-right-radius: 4px;
142
+ }
143
+
144
+ .message.assistant .message-bubble {
145
+ background: white;
146
+ color: var(--dark-color);
147
+ border: 1px solid #dee2e6;
148
+ border-bottom-left-radius: 4px;
149
+ }
150
+
151
+ .message.system .message-bubble {
152
+ background: #e8f4fd;
153
+ color: var(--primary-color);
154
+ border-left: 4px solid var(--success-color);
155
+ }
156
+
157
+ .message-header {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 8px;
161
+ margin-bottom: 6px;
162
+ font-size: 0.9rem;
163
+ }
164
+
165
+ .message.user .message-header {
166
+ justify-content: flex-end;
167
+ }
168
+
169
+ .message-content {
170
+ line-height: 1.6;
171
+ }
172
+
173
+ .message-content ul, .message-content ol {
174
+ padding-left: 1.5rem;
175
+ margin-bottom: 0.5rem;
176
+ }
177
+
178
+ /* Sources */
179
+ .sources-container {
180
+ margin-top: 1rem;
181
+ padding: 1rem;
182
+ background: rgba(0,0,0,0.03);
183
+ border-radius: 10px;
184
+ border-left: 4px solid var(--success-color);
185
+ }
186
+
187
+ .source-item {
188
+ padding: 8px 12px;
189
+ background: white;
190
+ border-radius: 8px;
191
+ margin-bottom: 8px;
192
+ font-size: 0.9rem;
193
+ border: 1px solid #dee2e6;
194
+ }
195
+
196
+ .confidence-badge {
197
+ display: inline-block;
198
+ background: var(--success-color);
199
+ color: white;
200
+ padding: 2px 8px;
201
+ border-radius: 12px;
202
+ font-size: 0.8rem;
203
+ margin-right: 8px;
204
+ }
205
+
206
+ /* Input Area */
207
+ .input-area {
208
+ padding: 1.5rem;
209
+ background: white;
210
+ border-top: 1px solid #dee2e6;
211
+ }
212
+
213
+ /* Quick Actions */
214
+ .quick-actions {
215
+ display: flex;
216
+ gap: 10px;
217
+ flex-wrap: wrap;
218
+ margin-bottom: 1rem;
219
+ }
220
+
221
+ .quick-action {
222
+ padding: 8px 16px;
223
+ background: #f8f9fa;
224
+ border: 1px solid #dee2e6;
225
+ border-radius: 20px;
226
+ font-size: 0.9rem;
227
+ transition: all 0.3s;
228
+ cursor: pointer;
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 6px;
232
+ }
233
+
234
+ .quick-action:hover {
235
+ background: var(--primary-color);
236
+ color: white;
237
+ border-color: var(--primary-color);
238
+ transform: translateY(-2px);
239
+ }
240
+
241
+ /* Ticket Items */
242
+ .ticket-item {
243
+ padding: 12px;
244
+ background: white;
245
+ border: 1px solid #dee2e6;
246
+ border-radius: 10px;
247
+ margin-bottom: 10px;
248
+ cursor: pointer;
249
+ transition: all 0.3s;
250
+ }
251
+
252
+ .ticket-item:hover {
253
+ border-color: var(--primary-color);
254
+ transform: translateX(5px);
255
+ }
256
+
257
+ .ticket-id {
258
+ font-weight: 600;
259
+ color: var(--primary-color);
260
+ font-size: 0.9rem;
261
+ }
262
+
263
+ .priority-badge {
264
+ padding: 3px 8px;
265
+ border-radius: 12px;
266
+ font-size: 0.8rem;
267
+ font-weight: 600;
268
+ }
269
+
270
+ .priority-urgent { background: #ffebee; color: #c62828; }
271
+ .priority-high { background: #fff3e0; color: #ef6c00; }
272
+ .priority-medium { background: #e8f5e9; color: #2e7d32; }
273
+ .priority-low { background: #f5f5f5; color: #616161; }
274
+
275
+ /* Loading Spinner */
276
+ .loading-spinner {
277
+ display: inline-block;
278
+ width: 20px;
279
+ height: 20px;
280
+ border: 3px solid #f3f3f3;
281
+ border-top: 3px solid var(--primary-color);
282
+ border-radius: 50%;
283
+ animation: spin 1s linear infinite;
284
+ }
285
+
286
+ @keyframes spin {
287
+ 0% { transform: rotate(0deg); }
288
+ 100% { transform: rotate(360deg); }
289
+ }
290
+
291
+ /* Notifications */
292
+ .notification {
293
+ position: fixed;
294
+ top: 20px;
295
+ right: 20px;
296
+ z-index: 10000;
297
+ animation: slideInRight 0.3s ease;
298
+ }
299
+
300
+ @keyframes slideInRight {
301
+ from { transform: translateX(100%); }
302
+ to { transform: translateX(0); }
303
+ }
304
+
305
+ /* Scrollbar */
306
+ ::-webkit-scrollbar {
307
+ width: 8px;
308
+ }
309
+
310
+ ::-webkit-scrollbar-track {
311
+ background: #f1f1f1;
312
+ }
313
+
314
+ ::-webkit-scrollbar-thumb {
315
+ background: #c1c1c1;
316
+ border-radius: 4px;
317
+ }
318
+
319
+ ::-webkit-scrollbar-thumb:hover {
320
+ background: #a1a1a1;
321
+ }
322
+
323
+ /* Responsive */
324
+ @media (max-width: 992px) {
325
+ .chat-container {
326
+ margin: 0;
327
+ height: 100vh;
328
+ border-radius: 0;
329
+ }
330
+
331
+ .sidebar {
332
+ display: none;
333
+ }
334
+
335
+ .message.user,
336
+ .message.assistant,
337
+ .message.system {
338
+ max-width: 85%;
339
+ }
340
+ }
341
+
342
+ @media (max-width: 768px) {
343
+ .message.user,
344
+ .message.assistant,
345
+ .message.system {
346
+ max-width: 90%;
347
+ }
348
+
349
+ .quick-actions {
350
+ justify-content: center;
351
+ }
352
+ }
353
+ </style>
354
+ </head>
355
+ <body>
356
+ <div class="chat-container">
357
+ <!-- Header -->
358
+ <div class="chat-header">
359
+ <div class="d-flex justify-content-between align-items-center">
360
+ <div>
361
+ <h1><i class="bi bi-robot"></i> ITSM Knowledge Bot</h1>
362
+ <div class="subtitle">Powered by AI & Semantic Search</div>
363
+ </div>
364
+ <div class="status-indicator">
365
+ <div class="status-dot"></div>
366
+ <span id="status-text">System Ready</span>
367
+ </div>
368
+ </div>
369
+ <div class="mt-2 d-flex justify-content-between align-items-center">
370
+ <div>
371
+ <small>Session: <span id="session-id" class="badge bg-light text-dark">{{ session_id }}</span></small>
372
+ <small class="ms-3">User: <span class="badge bg-info">Anonymous</span></small>
373
+ </div>
374
+ <div>
375
+ <button id="new-session" class="btn btn-sm btn-outline-light">
376
+ <i class="bi bi-plus-circle"></i> New Session
377
+ </button>
378
+ <button id="export-chat" class="btn btn-sm btn-outline-light ms-2">
379
+ <i class="bi bi-download"></i> Export
380
+ </button>
381
+ </div>
382
+ </div>
383
+ </div>
384
+
385
+ <!-- Main Body -->
386
+ <div class="chat-body">
387
+ <!-- Sidebar -->
388
+ <div class="sidebar">
389
+ <!-- Quick Actions -->
390
+ <div class="mb-4">
391
+ <h6 class="text-muted mb-3"><i class="bi bi-lightning"></i> Quick Actions</h6>
392
+ <div class="quick-actions">
393
+ <button class="quick-action" data-query="How to reset password?">
394
+ <i class="bi bi-key"></i> Password Reset
395
+ </button>
396
+ <button class="quick-action" data-query="VPN not working">
397
+ <i class="bi bi-wifi"></i> VPN Issues
398
+ </button>
399
+ <button class="quick-action" data-query="Email not syncing">
400
+ <i class="bi bi-envelope"></i> Email Problems
401
+ </button>
402
+ <button class="quick-action" data-query="Install software">
403
+ <i class="bi bi-download"></i> Software Install
404
+ </button>
405
+ </div>
406
+ <button id="create-ticket-btn" class="btn btn-primary w-100 mt-2">
407
+ <i class="bi bi-ticket"></i> Create Support Ticket
408
+ </button>
409
+ </div>
410
+
411
+ <!-- Chat History -->
412
+ <div class="mb-4">
413
+ <h6 class="text-muted mb-3"><i class="bi bi-clock-history"></i> Recent Messages</h6>
414
+ <div id="history-list">
415
+ <div class="text-center">
416
+ <small class="text-muted">No recent messages</small>
417
+ </div>
418
+ </div>
419
+ <button id="clear-history" class="btn btn-sm btn-outline-danger w-100 mt-2">
420
+ <i class="bi bi-trash"></i> Clear History
421
+ </button>
422
+ </div>
423
+
424
+ <!-- System Info -->
425
+ <div>
426
+ <h6 class="text-muted mb-3"><i class="bi bi-info-circle"></i> System Info</h6>
427
+ <div class="system-info">
428
+ <div class="d-flex justify-content-between mb-2">
429
+ <small>Knowledge Items:</small>
430
+ <small id="kb-count" class="text-primary">Loading...</small>
431
+ </div>
432
+ <div class="d-flex justify-content-between mb-2">
433
+ <small>AI Model:</small>
434
+ <small id="ai-model" class="text-success">Groq Llama 3</small>
435
+ </div>
436
+ <div class="d-flex justify-content-between">
437
+ <small>Status:</small>
438
+ <small id="system-status" class="text-success">● Operational</small>
439
+ </div>
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <!-- Chat Area -->
445
+ <div class="chat-area">
446
+ <!-- Messages Container -->
447
+ <div class="messages-container" id="messages-container">
448
+ <!-- Welcome Message -->
449
+ <div class="message system">
450
+ <div class="message-header">
451
+ <i class="bi bi-robot"></i>
452
+ <span class="fw-semibold">ITSM Bot</span>
453
+ <small class="text-muted">Just now</small>
454
+ </div>
455
+ <div class="message-bubble">
456
+ <div class="message-content">
457
+ <p>👋 <strong>Welcome to ITSM Knowledge Base Chatbot!</strong></p>
458
+ <p>I'm your AI-powered IT support assistant. I can help you with:</p>
459
+ <ul>
460
+ <li>🔍 Knowledge base searches</li>
461
+ <li>🔧 IT troubleshooting guides</li>
462
+ <li>🎫 Support ticket creation</li>
463
+ <li>🔒 Password reset procedures</li>
464
+ <li>🛠️ Software installation help</li>
465
+ <li>🌐 Network connectivity issues</li>
466
+ </ul>
467
+ <p class="mb-0">Try asking me questions like:</p>
468
+ <ul class="mb-0">
469
+ <li>"How do I reset my password?"</li>
470
+ <li>"My email is not working"</li>
471
+ <li>"How to connect to VPN?"</li>
472
+ <li>"Create a ticket for printer issues"</li>
473
+ </ul>
474
+ </div>
475
+ </div>
476
+ </div>
477
+ </div>
478
+
479
+ <!-- Input Area -->
480
+ <div class="input-area">
481
+ <div class="quick-actions mb-3">
482
+ <button class="quick-action" data-query="My email is not working">
483
+ <i class="bi bi-envelope-exclamation"></i> Email Issue
484
+ </button>
485
+ <button class="quick-action" data-query="How to connect to VPN?">
486
+ <i class="bi bi-shield-lock"></i> VPN Setup
487
+ </button>
488
+ <button class="quick-action" data-query="Printer not printing">
489
+ <i class="bi bi-printer"></i> Printer Help
490
+ </button>
491
+ <button class="quick-action" data-query="Software installation guide">
492
+ <i class="bi bi-gear"></i> Installation
493
+ </button>
494
+ </div>
495
+
496
+ <div class="input-group">
497
+ <textarea
498
+ id="message-input"
499
+ class="form-control"
500
+ placeholder="Type your IT question here... (Press Enter to send, Shift+Enter for new line)"
501
+ rows="3"
502
+ style="resize: none;"
503
+ ></textarea>
504
+ <button id="send-button" class="btn btn-primary">
505
+ <i class="bi bi-send"></i> Send
506
+ </button>
507
+ </div>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ </div>
512
+
513
+ <!-- Ticket Modal -->
514
+ <div class="modal fade" id="ticketModal" tabindex="-1">
515
+ <div class="modal-dialog modal-lg">
516
+ <div class="modal-content">
517
+ <div class="modal-header bg-primary text-white">
518
+ <h5 class="modal-title"><i class="bi bi-ticket"></i> Create Support Ticket</h5>
519
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
520
+ </div>
521
+ <div class="modal-body">
522
+ <div class="mb-3">
523
+ <label class="form-label fw-semibold">Issue Description *</label>
524
+ <textarea
525
+ id="ticket-description"
526
+ class="form-control"
527
+ placeholder="Describe your issue in detail. Include error messages, when it started, and what you've tried..."
528
+ rows="5"
529
+ ></textarea>
530
+ <div class="form-text">Be as detailed as possible for faster resolution.</div>
531
+ </div>
532
+
533
+ <div class="row">
534
+ <div class="col-md-6 mb-3">
535
+ <label class="form-label fw-semibold">Priority</label>
536
+ <select id="ticket-priority" class="form-select">
537
+ <option value="Low">Low - General inquiry</option>
538
+ <option value="Medium" selected>Medium - Minor issue</option>
539
+ <option value="High">High - Affecting work</option>
540
+ <option value="Urgent">Urgent - Critical system down</option>
541
+ </select>
542
+ </div>
543
+ <div class="col-md-6 mb-3">
544
+ <label class="form-label fw-semibold">Category</label>
545
+ <select id="ticket-category" class="form-select">
546
+ <option value="General">General IT Support</option>
547
+ <option value="Authentication">Password & Login</option>
548
+ <option value="Network">VPN & Network</option>
549
+ <option value="Software">Software & Applications</option>
550
+ <option value="Hardware">Hardware & Devices</option>
551
+ <option value="Communication">Email & Communication</option>
552
+ </select>
553
+ </div>
554
+ </div>
555
+
556
+ <div class="alert alert-info">
557
+ <i class="bi bi-info-circle"></i>
558
+ <strong>Ticket Information:</strong>
559
+ Once created, you'll receive a ticket ID and estimated resolution time.
560
+ </div>
561
+ </div>
562
+ <div class="modal-footer">
563
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
564
+ <button id="submit-ticket" class="btn btn-primary">
565
+ <i class="bi bi-check-circle"></i> Create Ticket
566
+ </button>
567
+ </div>
568
+ </div>
569
+ </div>
570
+ </div>
571
+
572
+ <!-- Bootstrap JS Bundle with Popper -->
573
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
574
+
575
+ <!-- Custom JavaScript -->
576
+ <script>
577
+ class ITSMChatbot {
578
+ constructor() {
579
+ this.sessionId = '{{ session_id }}';
580
+ this.currentTicketId = null;
581
+ this.setupEventListeners();
582
+ this.loadSystemInfo();
583
+ this.loadHistory();
584
+ }
585
+
586
+ setupEventListeners() {
587
+ // Send message
588
+ const sendButton = document.getElementById('send-button');
589
+ const messageInput = document.getElementById('message-input');
590
+
591
+ sendButton.addEventListener('click', () => this.sendMessage());
592
+ messageInput.addEventListener('keypress', (e) => {
593
+ if (e.key === 'Enter' && !e.shiftKey) {
594
+ e.preventDefault();
595
+ this.sendMessage();
596
+ }
597
+ });
598
+
599
+ // Auto-resize textarea
600
+ messageInput.addEventListener('input', function() {
601
+ this.style.height = 'auto';
602
+ this.style.height = Math.min(this.scrollHeight, 150) + 'px';
603
+ });
604
+
605
+ // Quick actions
606
+ document.querySelectorAll('.quick-action[data-query]').forEach(btn => {
607
+ btn.addEventListener('click', (e) => {
608
+ const query = e.currentTarget.getAttribute('data-query');
609
+ messageInput.value = query;
610
+ messageInput.focus();
611
+ // Optionally auto-send
612
+ // this.sendMessage();
613
+ });
614
+ });
615
+
616
+ // Create ticket button
617
+ document.getElementById('create-ticket-btn').addEventListener('click', () => {
618
+ this.openTicketModal();
619
+ });
620
+
621
+ // Ticket modal submit
622
+ document.getElementById('submit-ticket').addEventListener('click', () => {
623
+ this.createTicket();
624
+ });
625
+
626
+ // Clear history
627
+ document.getElementById('clear-history').addEventListener('click', () => {
628
+ this.clearHistory();
629
+ });
630
+
631
+ // New session
632
+ document.getElementById('new-session').addEventListener('click', () => {
633
+ this.createNewSession();
634
+ });
635
+
636
+ // Export chat
637
+ document.getElementById('export-chat').addEventListener('click', () => {
638
+ this.exportChat();
639
+ });
640
+ }
641
+
642
+ async sendMessage() {
643
+ const input = document.getElementById('message-input');
644
+ const message = input.value.trim();
645
+
646
+ if (!message) return;
647
+
648
+ // Add user message
649
+ this.addMessage('user', message);
650
+
651
+ // Clear input
652
+ input.value = '';
653
+ input.style.height = 'auto';
654
+
655
+ // Show loading
656
+ const loadingId = this.showLoading();
657
+
658
+ try {
659
+ const response = await fetch('/api/chat', {
660
+ method: 'POST',
661
+ headers: {
662
+ 'Content-Type': 'application/json',
663
+ 'Accept': 'application/json'
664
+ },
665
+ body: JSON.stringify({
666
+ message: message,
667
+ session_id: this.sessionId
668
+ })
669
+ });
670
+
671
+ const data = await response.json();
672
+
673
+ // Remove loading
674
+ this.removeLoading(loadingId);
675
+
676
+ if (data.success) {
677
+ // Add bot response
678
+ this.addMessage('assistant', data.response, data.sources, data.intent);
679
+
680
+ // Update history
681
+ this.updateHistoryList();
682
+
683
+ // Auto-suggest ticket for issue reports
684
+ if (data.intent === 'issue_report') {
685
+ setTimeout(() => this.showTicketSuggestion(message), 1500);
686
+ }
687
+
688
+ // Play notification sound
689
+ this.playNotification();
690
+ } else {
691
+ this.showNotification(data.error || 'Error sending message', 'danger');
692
+ }
693
+ } catch (error) {
694
+ console.error('Error:', error);
695
+ this.removeLoading(loadingId);
696
+ this.showNotification('Network error. Please check your connection.', 'danger');
697
+ }
698
+ }
699
+
700
+ addMessage(role, content, sources = [], intent = null) {
701
+ const container = document.getElementById('messages-container');
702
+ const messageDiv = document.createElement('div');
703
+ messageDiv.className = `message ${role}`;
704
+
705
+ const time = new Date().toLocaleTimeString([], {
706
+ hour: '2-digit', minute: '2-digit'
707
+ });
708
+
709
+ const sender = role === 'user' ? 'You' : 'ITSM Bot';
710
+ const icon = role === 'user' ? 'bi-person' : 'bi-robot';
711
+ const color = role === 'user' ? '#4361ee' : '#d81b60';
712
+
713
+ // Format content
714
+ let formattedContent = this.formatMessage(content);
715
+
716
+ // Add sources if available
717
+ let sourcesHtml = '';
718
+ if (sources && sources.length > 0) {
719
+ sourcesHtml = `
720
+ <div class="sources-container">
721
+ <small class="text-muted mb-2 d-block fw-semibold">
722
+ <i class="bi bi-book"></i> Knowledge Sources:
723
+ </small>
724
+ ${sources.map(source => `
725
+ <div class="source-item">
726
+ <span class="confidence-badge">${source.confidence}</span>
727
+ <strong>${source.topic}</strong>
728
+ <small class="text-muted d-block">${source.category}</small>
729
+ </div>
730
+ `).join('')}
731
+ </div>
732
+ `;
733
+ }
734
+
735
+ // Add intent badge if available
736
+ let intentBadge = '';
737
+ if (intent && intent !== 'knowledge_request') {
738
+ const intentLabels = {
739
+ 'issue_report': 'Issue Report',
740
+ 'password_reset': 'Password Help',
741
+ 'software_install': 'Installation Guide',
742
+ 'greeting': 'Greeting'
743
+ };
744
+ if (intentLabels[intent]) {
745
+ intentBadge = `<span class="badge bg-info float-end">${intentLabels[intent]}</span>`;
746
+ }
747
+ }
748
+
749
+ messageDiv.innerHTML = `
750
+ <div class="message-header">
751
+ <i class="bi ${icon}" style="color: ${color}"></i>
752
+ <span class="fw-semibold">${sender}</span>
753
+ ${intentBadge}
754
+ <small class="text-muted ms-auto">${time}</small>
755
+ </div>
756
+ <div class="message-bubble">
757
+ <div class="message-content">
758
+ ${formattedContent}
759
+ ${sourcesHtml}
760
+ </div>
761
+ </div>
762
+ `;
763
+
764
+ container.appendChild(messageDiv);
765
+ container.scrollTop = container.scrollHeight;
766
+ }
767
+
768
+ formatMessage(content) {
769
+ // Convert markdown-like formatting
770
+ return content
771
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
772
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
773
+ .replace(/`(.*?)`/g, '<code>$1</code>')
774
+ .replace(/\n\n/g, '</p><p>')
775
+ .replace(/\n/g, '<br>')
776
+ .replace(/^\s*[-•*]\s+/gm, '• ') // Convert list items
777
+ .replace(/^\s*\d+\.\s+/gm, (match) => `<br>${match}`) // Numbered lists
778
+ .replace(/^(Step \d+:|Note:|Tip:|Warning:)/gim, '<strong>$1</strong>');
779
+ }
780
+
781
+ showLoading() {
782
+ const container = document.getElementById('messages-container');
783
+ const loadingDiv = document.createElement('div');
784
+ loadingDiv.id = 'loading-indicator';
785
+ loadingDiv.className = 'message assistant';
786
+ loadingDiv.innerHTML = `
787
+ <div class="message-header">
788
+ <i class="bi bi-robot" style="color: #d81b60"></i>
789
+ <span class="fw-semibold">ITSM Bot</span>
790
+ <small class="text-muted">Thinking...</small>
791
+ </div>
792
+ <div class="message-bubble">
793
+ <div class="text-center py-2">
794
+ <div class="loading-spinner d-inline-block"></div>
795
+ <small class="text-muted d-block mt-2">Searching knowledge base...</small>
796
+ </div>
797
+ </div>
798
+ `;
799
+ container.appendChild(loadingDiv);
800
+ container.scrollTop = container.scrollHeight;
801
+ return 'loading-indicator';
802
+ }
803
+
804
+ removeLoading(id) {
805
+ const element = document.getElementById(id);
806
+ if (element) element.remove();
807
+ }
808
+
809
+ async loadSystemInfo() {
810
+ try {
811
+ const response = await fetch('/api/health');
812
+ const data = await response.json();
813
+
814
+ if (data.status === 'healthy') {
815
+ document.getElementById('kb-count').textContent = data.knowledge_items + ' items';
816
+ document.getElementById('system-status').textContent = '● Operational';
817
+ document.getElementById('system-status').className = 'text-success';
818
+
819
+ if (data.groq_available) {
820
+ document.getElementById('ai-model').textContent = 'Groq AI Enabled';
821
+ } else {
822
+ document.getElementById('ai-model').textContent = 'Simple Mode';
823
+ document.getElementById('ai-model').className = 'text-warning';
824
+ }
825
+ }
826
+ } catch (error) {
827
+ console.error('Error loading system info:', error);
828
+ }
829
+ }
830
+
831
+ async loadHistory() {
832
+ try {
833
+ const response = await fetch(`/api/history/${this.sessionId}`);
834
+ const data = await response.json();
835
+
836
+ if (data.success && data.messages.length > 0) {
837
+ this.updateHistoryList();
838
+ }
839
+ } catch (error) {
840
+ console.error('Error loading history:', error);
841
+ }
842
+ }
843
+
844
+ async updateHistoryList() {
845
+ try {
846
+ const response = await fetch(`/api/history/${this.sessionId}`);
847
+ const data = await response.json();
848
+
849
+ if (data.success) {
850
+ const historyList = document.getElementById('history-list');
851
+ const recent = data.messages.slice(-5).reverse();
852
+
853
+ if (recent.length === 0) {
854
+ historyList.innerHTML = `
855
+ <div class="text-center py-3">
856
+ <small class="text-muted">No recent messages</small>
857
+ </div>
858
+ `;
859
+ return;
860
+ }
861
+
862
+ historyList.innerHTML = recent.map(msg => `
863
+ <div class="ticket-item mb-2" onclick="chatbot.loadMessage('${msg.timestamp}')">
864
+ <div class="d-flex justify-content-between align-items-center">
865
+ <small class="text-truncate">
866
+ <i class="bi bi-${msg.role === 'user' ? 'person' : 'robot'} me-1"></i>
867
+ ${msg.content.substring(0, 40)}${msg.content.length > 40 ? '...' : ''}
868
+ </small>
869
+ <small class="text-muted">${new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</small>
870
+ </div>
871
+ </div>
872
+ `).join('');
873
+ }
874
+ } catch (error) {
875
+ console.error('Error updating history:', error);
876
+ }
877
+ }
878
+
879
+ async clearHistory() {
880
+ if (!confirm('Are you sure you want to clear all chat history? This action cannot be undone.')) {
881
+ return;
882
+ }
883
+
884
+ try {
885
+ const response = await fetch(`/api/history/${this.sessionId}`, {
886
+ method: 'DELETE'
887
+ });
888
+
889
+ const data = await response.json();
890
+
891
+ if (data.success) {
892
+ // Clear messages container
893
+ const container = document.getElementById('messages-container');
894
+ container.innerHTML = `
895
+ <div class="message system">
896
+ <div class="message-header">
897
+ <i class="bi bi-robot"></i>
898
+ <span class="fw-semibold">ITSM Bot</span>
899
+ <small class="text-muted">Just now</small>
900
+ </div>
901
+ <div class="message-bubble">
902
+ <div class="message-content">
903
+ <p>Chat history cleared. How can I help you today?</p>
904
+ </div>
905
+ </div>
906
+ </div>
907
+ `;
908
+
909
+ // Update history list
910
+ this.updateHistoryList();
911
+
912
+ this.showNotification('Chat history cleared successfully', 'success');
913
+ } else {
914
+ this.showNotification('Failed to clear history', 'danger');
915
+ }
916
+ } catch (error) {
917
+ console.error('Error clearing history:', error);
918
+ this.showNotification('Error clearing history', 'danger');
919
+ }
920
+ }
921
+
922
+ async createTicket() {
923
+ const description = document.getElementById('ticket-description').value.trim();
924
+ const priority = document.getElementById('ticket-priority').value;
925
+ const category = document.getElementById('ticket-category').value;
926
+
927
+ if (!description) {
928
+ this.showNotification('Please enter a description for the ticket', 'warning');
929
+ document.getElementById('ticket-description').focus();
930
+ return;
931
+ }
932
+
933
+ try {
934
+ const response = await fetch('/api/ticket', {
935
+ method: 'POST',
936
+ headers: {
937
+ 'Content-Type': 'application/json',
938
+ 'Accept': 'application/json'
939
+ },
940
+ body: JSON.stringify({
941
+ description: description,
942
+ session_id: this.sessionId,
943
+ priority: priority,
944
+ category: category
945
+ })
946
+ });
947
+
948
+ const data = await response.json();
949
+
950
+ if (data.success) {
951
+ // Close modal
952
+ const modal = bootstrap.Modal.getInstance(document.getElementById('ticketModal'));
953
+ modal.hide();
954
+
955
+ // Clear form
956
+ document.getElementById('ticket-description').value = '';
957
+ document.getElementById('ticket-priority').value = 'Medium';
958
+ document.getElementById('ticket-category').value = 'General';
959
+
960
+ // Show success message in chat
961
+ const ticket = data.ticket;
962
+ this.addMessage('system',
963
+ `✅ <strong>Support Ticket Created Successfully!</strong><br><br>
964
+ <strong>Ticket ID:</strong> ${ticket.id}<br>
965
+ <strong>Priority:</strong> <span class="priority-badge priority-${ticket.priority.toLowerCase()}">${ticket.priority}</span><br>
966
+ <strong>Category:</strong> ${ticket.category}<br>
967
+ <strong>Status:</strong> ${ticket.status}<br>
968
+ <strong>Estimated Resolution:</strong> ${ticket.estimated_resolution}<br><br>
969
+ Your ticket has been logged and will be addressed by our IT team.`);
970
+
971
+ this.showNotification(`Ticket ${ticket.id} created successfully!`, 'success');
972
+
973
+ } else {
974
+ this.showNotification(data.error || 'Failed to create ticket', 'danger');
975
+ }
976
+ } catch (error) {
977
+ console.error('Error creating ticket:', error);
978
+ this.showNotification('Network error creating ticket', 'danger');
979
+ }
980
+ }
981
+
982
+ showTicketSuggestion(issue) {
983
+ const container = document.getElementById('messages-container');
984
+ const suggestion = document.createElement('div');
985
+ suggestion.className = 'message system';
986
+ suggestion.innerHTML = `
987
+ <div class="message-header">
988
+ <i class="bi bi-lightbulb"></i>
989
+ <span class="fw-semibold">Suggestion</span>
990
+ </div>
991
+ <div class="message-bubble">
992
+ <p>It seems like you're reporting an issue. Would you like me to create a support ticket for this?</p>
993
+ <div class="mt-2">
994
+ <button class="btn btn-sm btn-primary me-2" id="accept-ticket-suggestion">
995
+ <i class="bi bi-check-circle"></i> Yes, Create Ticket
996
+ </button>
997
+ <button class="btn btn-sm btn-outline-secondary" id="decline-ticket-suggestion">
998
+ <i class="bi bi-x-circle"></i> No, Thanks
999
+ </button>
1000
+ </div>
1001
+ </div>
1002
+ `;
1003
+ container.appendChild(suggestion);
1004
+ container.scrollTop = container.scrollHeight;
1005
+
1006
+ // Add event listeners
1007
+ document.getElementById('accept-ticket-suggestion').addEventListener('click', () => {
1008
+ document.getElementById('ticket-description').value = issue;
1009
+ this.openTicketModal();
1010
+ suggestion.remove();
1011
+ });
1012
+
1013
+ document.getElementById('decline-ticket-suggestion').addEventListener('click', () => {
1014
+ suggestion.remove();
1015
+ });
1016
+ }
1017
+
1018
+ openTicketModal() {
1019
+ const modal = new bootstrap.Modal(document.getElementById('ticketModal'));
1020
+ modal.show();
1021
+ document.getElementById('ticket-description').focus();
1022
+ }
1023
+
1024
+ async createNewSession() {
1025
+ try {
1026
+ const response = await fetch('/api/session', {
1027
+ method: 'POST',
1028
+ headers: {
1029
+ 'Content-Type': 'application/json',
1030
+ 'Accept': 'application/json'
1031
+ },
1032
+ body: JSON.stringify({ user_id: 'web_user' })
1033
+ });
1034
+
1035
+ const data = await response.json();
1036
+
1037
+ if (data.success) {
1038
+ this.sessionId = data.session_id;
1039
+ document.getElementById('session-id').textContent = this.sessionId;
1040
+
1041
+ // Clear chat
1042
+ const container = document.getElementById('messages-container');
1043
+ container.innerHTML = `
1044
+ <div class="message system">
1045
+ <div class="message-header">
1046
+ <i class="bi bi-robot"></i>
1047
+ <span class="fw-semibold">ITSM Bot</span>
1048
+ <small class="text-muted">Just now</small>
1049
+ </div>
1050
+ <div class="message-bubble">
1051
+ <div class="message-content">
1052
+ <p>👋 New session started! How can I help you today?</p>
1053
+ </div>
1054
+ </div>
1055
+ </div>
1056
+ `;
1057
+
1058
+ // Update history
1059
+ this.updateHistoryList();
1060
+
1061
+ this.showNotification('New session created', 'success');
1062
+ }
1063
+ } catch (error) {
1064
+ console.error('Error creating session:', error);
1065
+ this.showNotification('Failed to create session', 'danger');
1066
+ }
1067
+ }
1068
+
1069
+ async exportChat() {
1070
+ try {
1071
+ const response = await fetch(`/api/history/${this.sessionId}`);
1072
+ const data = await response.json();
1073
+
1074
+ if (data.success) {
1075
+ const exportData = {
1076
+ session_id: this.sessionId,
1077
+ exported_at: new Date().toISOString(),
1078
+ message_count: data.messages.length,
1079
+ messages: data.messages
1080
+ };
1081
+
1082
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], {
1083
+ type: 'application/json'
1084
+ });
1085
+
1086
+ const url = URL.createObjectURL(blob);
1087
+ const a = document.createElement('a');
1088
+ a.href = url;
1089
+ a.download = `itsm-chat-${this.sessionId}-${new Date().toISOString().slice(0,10)}.json`;
1090
+ document.body.appendChild(a);
1091
+ a.click();
1092
+ document.body.removeChild(a);
1093
+ URL.revokeObjectURL(url);
1094
+
1095
+ this.showNotification('Chat exported successfully', 'success');
1096
+ }
1097
+ } catch (error) {
1098
+ console.error('Error exporting chat:', error);
1099
+ this.showNotification('Failed to export chat', 'danger');
1100
+ }
1101
+ }
1102
+
1103
+ showNotification(message, type = 'success') {
1104
+ // Remove existing notifications
1105
+ document.querySelectorAll('.alert.notification').forEach(alert => alert.remove());
1106
+
1107
+ // Create new notification
1108
+ const alert = document.createElement('div');
1109
+ alert.className = `alert alert-${type} alert-dismissible fade show notification`;
1110
+ alert.innerHTML = `
1111
+ ${message}
1112
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
1113
+ `;
1114
+
1115
+ document.body.appendChild(alert);
1116
+
1117
+ // Auto-remove after 5 seconds
1118
+ setTimeout(() => {
1119
+ if (alert.parentNode) {
1120
+ alert.remove();
1121
+ }
1122
+ }, 5000);
1123
+ }
1124
+
1125
+ playNotification() {
1126
+ // Simple notification sound using Web Audio API
1127
+ try {
1128
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
1129
+ const oscillator = audioContext.createOscillator();
1130
+ const gainNode = audioContext.createGain();
1131
+
1132
+ oscillator.connect(gainNode);
1133
+ gainNode.connect(audioContext.destination);
1134
+
1135
+ oscillator.frequency.value = 800;
1136
+ oscillator.type = 'sine';
1137
+
1138
+ gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
1139
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
1140
+
1141
+ oscillator.start(audioContext.currentTime);
1142
+ oscillator.stop(audioContext.currentTime + 0.5);
1143
+ } catch (e) {
1144
+ // Audio not supported, silently fail
1145
+ }
1146
+ }
1147
+
1148
+ loadMessage(timestamp) {
1149
+ // This would load a specific message in a real implementation
1150
+ console.log('Load message:', timestamp);
1151
+ }
1152
+ }
1153
+
1154
+ // Initialize chatbot when page loads
1155
+ document.addEventListener('DOMContentLoaded', () => {
1156
+ window.chatbot = new ITSMChatbot();
1157
+ });
1158
+ </script>
1159
+ </body>
1160
+ </html>