k96beni commited on
Commit
1a7cde6
·
verified ·
1 Parent(s): 11f1694

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1057 -586
app.py CHANGED
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
  import os
3
  import json
4
  import time
@@ -37,743 +36,1215 @@ log_file_path = os.path.join(log_folder, "conversation_log_v2.txt")
37
 
38
  # Skapa en tom loggfil om den inte finns
39
  if not os.path.exists(log_file_path):
40
- try:
41
- with open(log_file_path, "w", encoding="utf-8") as f:
42
- f.write("") # Skapa en tom fil
43
- print(f"Skapade tom loggfil: {log_file_path}")
44
- except IOError as e:
45
- print(f"Kunde inte skapa loggfil {log_file_path}: {e}")
46
- # Överväg att avsluta om loggning är kritisk
47
- # exit(1)
48
-
49
 
50
  hf_token = os.environ.get("HF_TOKEN")
51
- # Validera HF_TOKEN endast om vi är i Hugging Face-miljön
52
- if IS_HUGGINGFACE and not hf_token:
53
- raise ValueError("HF_TOKEN saknas (krävs i Hugging Face-miljön)")
54
 
55
- # Konfigurera CommitScheduler endast om vi är i Hugging Face-miljön
56
- commit_scheduler = None # Initiera till None
57
- if IS_HUGGINGFACE and hf_token:
58
- try:
59
- commit_scheduler = CommitScheduler(
60
- repo_id="ChargeNodeEurope/logfiles", # Se till att detta repo finns och att token har rättigheter
61
- repo_type="dataset",
62
- folder_path=log_folder, # Se till att denna mapp existerar och är skrivbar
63
- path_in_repo="logs_v2", # Mappsökväg i dataset-repot
64
- every=300, # Committa var 5:e minut (300 sekunder)
65
- token=hf_token
66
- )
67
- print(f"CommitScheduler konfigurerad för att pusha '{log_folder}' till repo 'ChargeNodeEurope/logfiles' var 5:e minut.")
68
- except Exception as e:
69
- print(f"Kunde inte initiera CommitScheduler: {e}. Loggar kommer inte att sparas automatiskt till Hub.")
70
- commit_scheduler = None # Säkerställ att den är None vid fel
71
- else:
72
- print("Info: Kör lokalt eller HF_TOKEN saknas/ogiltig. CommitScheduler är inte aktiv.")
73
 
74
 
75
  # --- Globala variabler ---
76
- last_log = None
 
77
  last_log_lock = threading.Lock()
78
 
79
  # --- Förbättrad loggfunktion ---
80
  def safe_append_to_log(log_entry):
81
- """Säker metod för att lägga till loggdata med rotation och sanering."""
82
  try:
 
83
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
84
-
85
- # Loggrotation (valfritt men rekommenderat)
86
  try:
87
- if os.path.exists(log_file_path):
88
- log_size = os.path.getsize(log_file_path)
89
- if log_size > 10 * 1024 * 1024: # 10 MB gräns
90
- timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
91
- backup_path = f"{log_file_path}.{timestamp_suffix}.bak"
92
- os.rename(log_file_path, backup_path)
93
- print(f"Loggfil roterad: {log_file_path} -> {backup_path}")
94
- with open(log_file_path, "w", encoding="utf-8") as f: f.write("")
95
- except FileNotFoundError: pass
96
- except Exception as e: print(f"Fel vid loggrotation: {e}")
97
-
98
- # Sanering
99
- sanitized_entry = {k: (v.replace('\0', '')[:20000] if isinstance(v, str) else v) for k, v in log_entry.items()}
100
-
101
- # Skriv till fil
 
 
 
 
 
 
 
102
  with open(log_file_path, "a", encoding="utf-8") as log_file:
103
- log_json = json.dumps(sanitized_entry, ensure_ascii=False)
104
  log_file.write(log_json + "\n")
105
- log_file.flush()
106
-
 
107
  return True
 
108
  except Exception as e:
109
- print(f"Allvarligt fel vid loggning: {e}")
 
 
110
  try:
111
- with open(os.path.join(log_folder, "logging_errors.txt"), "a", encoding="utf-8") as error_file:
112
- error_file.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Loggningsfel: {e}\nOriginalpost (delvis): {str(log_entry)[:500]}\n")
113
- except Exception as ef: print(f"Kunde inte ens logga loggningsfelet: {ef}")
114
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  # --- Laddar textkällor ---
117
  def load_local_files():
118
  uploaded_text = ""
119
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
120
- excluded_prefixes = ["requirements", "app", "conversation_log", "secrets", "logging_errors"]
121
- current_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.'
122
-
123
- print(f"Söker efter filer i: {current_dir}")
124
- items_in_dir = os.listdir(current_dir)
125
- print(f"Hittade {len(items_in_dir)} objekt.")
126
-
127
- files_processed = 0
128
- for item in items_in_dir:
129
- item_path = os.path.join(current_dir, item)
130
- if not os.path.isfile(item_path): continue
131
-
132
- is_allowed = item.lower().endswith(tuple(allowed))
133
- is_excluded = any(item.lower().startswith(prefix) for prefix in excluded_prefixes)
134
-
135
- if is_allowed and not is_excluded:
136
- print(f"Bearbetar fil: {item}")
137
- files_processed += 1
138
  try:
139
- content = ""
140
- if item.endswith(".txt"):
141
- with open(item_path, "r", encoding="utf-8", errors='replace') as f: content = f.read()
142
- elif item.endswith(".docx"):
143
- try:
144
- from docx import Document
145
- content = "\n".join([p.text for p in Document(item_path).paragraphs])
146
- except ImportError: print(f"Varning: 'python-docx' behövs för {item}.")
147
- elif item.endswith(".pdf"):
148
- try:
149
- import PyPDF2
150
- with open(item_path, "rb") as f:
151
- reader = PyPDF2.PdfReader(f, strict=False) # strict=False kan hjälpa med korrupta filer
152
- if reader.is_encrypted:
153
- print(f"Varning: PDF {item} är krypterad.")
154
- try: # Försök dekryptera med tomt lösenord (vanligt)
155
- if reader.decrypt('') == 0: # 0 = misslyckades
156
- continue # Hoppa över filen
157
- except NotImplementedError:
158
- print(f"Varning: Krypteringsmetoden för PDF {item} stöds inte av PyPDF2.")
159
- continue
160
- temp_content = []
161
- for i, page in enumerate(reader.pages):
162
- try:
163
- text = page.extract_text()
164
- if text: temp_content.append(text)
165
- except Exception as page_err:
166
- print(f"Fel vid extrahering av sida {i+1} från PDF {item}: {page_err}")
167
- content = "\n\n".join(temp_content) # Separera sidor tydligare
168
- except ImportError: print(f"Varning: 'PyPDF2' behövs för {item}.")
169
- except Exception as pdf_error: print(f"Fel vid läsning av PDF {item}: {str(pdf_error)}")
170
- elif item.endswith(".csv"):
171
- try: content = pd.read_csv(item_path, encoding='utf-8', low_memory=False).to_string()
172
- except UnicodeDecodeError:
173
- try: content = pd.read_csv(item_path, encoding='latin1', low_memory=False).to_string()
174
- except Exception as csv_err: print(f"Fel vid läsning av CSV {item} (även med latin1): {csv_err}")
175
- except pd.errors.EmptyDataError: print(f"Varning: CSV {item} är tom.")
176
- except Exception as csv_err: print(f"Fel vid läsning av CSV {item}: {csv_err}")
177
- elif item.endswith((".xls", ".xlsx")):
178
- try:
179
- df = pd.read_excel(item_path, sheet_name=None) # Läs alla ark
180
- all_sheets_content = []
181
- for sheet_name, sheet_df in df.items():
182
- sheet_content = ""
183
- if item.lower() == "faq stadat.xlsx" and 'Fråga' in sheet_df.columns and 'Svar' in sheet_df.columns:
184
- rows = [f"Fråga: {str(row['Fråga']) if pd.notna(row['Fråga']) else ''}\nSvar: {str(row['Svar']) if pd.notna(row['Svar']) else ''}"
185
- for index, row in sheet_df.iterrows() if pd.notna(row['Fråga']) or pd.notna(row['Svar'])]
186
- sheet_content = "\n\n".join(rows)
187
- else:
188
- sheet_content = sheet_df.to_string()
189
- all_sheets_content.append(f"--- Ark: {sheet_name} ---\n{sheet_content}")
190
- content = "\n\n".join(all_sheets_content)
191
- except ImportError: print(f"Varning: 'openpyxl'/'xlrd' kan behövas för {item}.")
192
- except Exception as excel_err: print(f"Fel vid läsning av Excel {item}: {excel_err}")
193
-
194
- if content and isinstance(content, str):
195
- uploaded_text += f"\n\n--- FIL: {item} ---\n{content}"
196
- elif not content: print(f"Info: Ingen text extraherad från {item}")
197
- except Exception as e: print(f"Allmänt fel vid bearbetning av {item}: {str(e)}")
198
-
199
- if files_processed == 0: print("Varning: Inga filer hittades att ladda.")
200
- else: print(f"Bearbetade {files_processed} filer.")
201
  return uploaded_text.strip()
202
 
203
  def load_prompt():
204
- prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.', "prompt.txt")
205
  try:
206
- with open(prompt_path, "r", encoding="utf-8") as f: return f.read().strip()
207
- except FileNotFoundError:
208
- print(f"Info: prompt.txt ej hittad på {prompt_path}. Använder standardprompt.")
209
- return "Du är en hjälpsam AI-assistent för ChargeNode. Svara kortfattat och vänligt baserat på den givna kontexten. Om kontext saknas eller inte är relevant, säg att du inte har informationen i dina dokument och hänvisa till supporten."
210
  except Exception as e:
211
- print(f"Fel vid läsning av prompt.txt: {e}")
212
- return "Du är en hjälpsam AI-assistent." # Nöd-fallback
213
 
214
  prompt_template = load_prompt()
215
 
216
  # Förbered textsegment
217
  def prepare_chunks(text_data):
218
- global chunks, chunk_sources
219
- local_chunks, local_sources = [], []
220
  for source, text in text_data.items():
221
- if not isinstance(text, str) or not text.strip(): continue
222
- # Smartare splittning
223
- paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
224
- if len(paragraphs) < 5: # Om få dubbla radbrytningar, prova enkel
225
- paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
226
- if not paragraphs: paragraphs = [text.strip()] # Om inga radbrytningar alls
227
-
228
- current_chunk = ""
229
  for para in paragraphs:
230
- if not para: continue
231
- if len(para) > MAX_CHUNK_SIZE:
232
- if current_chunk:
233
- local_chunks.append(current_chunk); local_sources.append(source)
234
- current_chunk = ""
235
- # Dela lång paragraf
236
- for i in range(0, len(para), MAX_CHUNK_SIZE):
237
- local_chunks.append(para[i : i + MAX_CHUNK_SIZE]); local_sources.append(source)
238
- elif len(current_chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
239
- current_chunk += (" " + para if current_chunk else para)
240
  else:
241
- if current_chunk: local_chunks.append(current_chunk); local_sources.append(source)
242
- current_chunk = para
243
- if current_chunk: local_chunks.append(current_chunk); local_sources.append(source)
244
-
245
- chunks = local_chunks; chunk_sources = local_sources
246
- return chunks, chunk_sources
247
-
248
- # --- Embeddings ---
249
- embedder = None; embeddings = None; index = None
250
- chunks = []; chunk_sources = []
 
 
 
 
 
251
  embedding_lock = threading.Lock()
252
 
253
  def initialize_embeddings():
254
- """Initierar SentenceTransformer och FAISS-index."""
255
  global embedder, embeddings, index, chunks, chunk_sources
 
 
256
  with embedding_lock:
257
- if embedder is not None: return # Redan klar
258
- print("Initierar Embeddings...")
259
- try:
260
- text_data = {"local_files": load_local_files()}
261
- if not text_data["local_files"]:
262
- print("Varning: Ingen textdata för indexering.")
263
- embedder = SentenceTransformer('all-MiniLM-L6-v2')
264
- dim = embedder.get_sentence_embedding_dimension()
265
- index = faiss.IndexFlatIP(dim)
266
- chunks, chunk_sources = [], []
267
- embeddings = np.array([], dtype=np.float32).reshape(0, dim)
268
- print("Tomt index skapat.")
269
- return
270
-
271
- chunks, chunk_sources = prepare_chunks(text_data)
272
- print(f"{len(chunks)} segment förberedda.")
273
- if not chunks:
274
- print("Varning: Inga segment skapades.")
275
- embedder = SentenceTransformer('all-MiniLM-L6-v2')
276
- dim = embedder.get_sentence_embedding_dimension()
277
- index = faiss.IndexFlatIP(dim)
278
- embeddings = np.array([], dtype=np.float32).reshape(0, dim)
279
- print("Tomt index skapat.")
280
- return
281
-
282
- print("Skapar embeddings (kan ta tid)...")
283
- embedder = SentenceTransformer('all-MiniLM-L6-v2')
284
- embeddings_np = embedder.encode(chunks, convert_to_numpy=True, show_progress_bar=True)
285
- if embeddings_np.size == 0: raise ValueError("Encoding resulterade i tomma embeddings.")
286
-
287
- norms = np.linalg.norm(embeddings_np, axis=1, keepdims=True)
288
- norms[norms == 0] = 1e-10
289
- embeddings_np /= norms
290
- embeddings = embeddings_np.astype(np.float32) # Spara som global variabel
291
-
292
- dim = embeddings.shape[1]
293
- index = faiss.IndexFlatIP(dim)
294
- index.add(embeddings)
295
- print(f"FAISS-index klart ({index.ntotal} vektorer, dim {dim}).")
296
-
297
- except Exception as e:
298
- print(f"FATALT FEL vid initiering av embeddings: {e}")
299
- embedder, embeddings, index, chunks, chunk_sources = None, None, None, [], []
300
- raise RuntimeError(f"Kunde inte initiera embeddings: {e}") from e
301
-
 
 
 
 
 
302
  def retrieve_context(query, k=RETRIEVAL_K):
303
- """Hämtar relevant kontext."""
304
- # initialize_embeddings() # Bör redan vara anropad vid start
305
- if index is None or embedder is None or index.ntotal == 0:
306
- print("Varning: Försöker hämta kontext när index inte är redo.")
307
- return "Ingen kontext tillgänglig (index ej redo).", ["error"]
308
  try:
 
 
 
 
309
  query_embedding = embedder.encode([query], convert_to_numpy=True)
310
- if query_embedding.size == 0: return "Kunde inte bearbeta frågan.", ["error"]
311
- norm = np.linalg.norm(query_embedding); query_embedding /= (norm if norm != 0 else 1e-10)
312
- query_embedding = query_embedding.astype(np.float32)
313
-
 
 
 
 
 
 
 
 
 
 
314
  actual_k = min(k, index.ntotal)
315
- if actual_k == 0: return "", ["no_context"]
316
-
 
317
  D, I = index.search(query_embedding, actual_k)
318
-
319
- retrieved = [f"[Dokument {r+1}] {chunks[idx]}" for r, idx in enumerate(I[0]) if 0 <= idx < len(chunks)]
320
- sources = list({chunk_sources[idx] for idx in I[0] if 0 <= idx < len(chunk_sources)})
321
-
322
- if not retrieved: return "", (["no_context"] + sources) if sources else ["no_context"]
323
- return "\n---\n".join(retrieved), sources
 
 
 
 
 
 
324
  except Exception as e:
325
  print(f"Fel vid hämtning av kontext: {e}")
326
- return f"Fel vid kontexthämtning: {str(e)[:100]}", ["error"]
327
 
328
  def generate_answer(query):
329
- """Genererar svar baserat på fråga och kontext."""
330
  context, sources = retrieve_context(query)
331
- system_content = prompt_template
332
- user_prompt = f"Kontext:\n{context if context else 'Ingen specifik kontext hittades.'}\n\nFråga: {query}"
333
-
334
- if "error" in sources:
335
- system_content += "\n(Varning: Fel vid kontexthämtning.)"
336
- elif "no_context" in sources:
337
- system_content += "\n(Info: Ingen specifik lokal kontext hittades.)"
338
-
339
  try:
340
  response = client.chat.completions.create(
341
  model="gpt-3.5-turbo",
342
- messages=[{"role": "system", "content": system_content}, {"role": "user", "content": user_prompt}],
343
- temperature=0.2, max_tokens=500
 
 
 
 
344
  )
345
- answer = response.choices[0].message.content.strip()
346
- answer += "\n\n*AI-genererat svar.* För personlig hjälp, kontakta support@chargenode.eu / 010-205 10 55."
347
- return answer
348
  except Exception as e:
349
- print(f"Fel vid OpenAI API-anrop: {e}")
350
- return f"Tekniskt fel vid svar ({type(e).__name__}). Försök igen eller kontakta support.\n\n*AI-genererat svar.* support@chargenode.eu / 010-2051055."
351
 
352
  # --- Slack Integration ---
353
  def send_to_slack(subject, content, color="#2a9d8f"):
 
354
  webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
355
- if not webhook_url: print("Slack webhook URL saknas."); return False
 
 
 
356
  try:
357
- safe_subject = subject[:150]; safe_content = content[:2900]
358
- if len(content) > 2900: safe_content += "\n... (meddelandet trunkerat)"
359
- payload = {"attachments": [{"color": color,"blocks": [{"type": "header","text": {"type": "plain_text","text": safe_subject,"emoji": True}},{"type": "divider"},{"type": "section","text": {"type": "mrkdwn","text": safe_content}}]}]}
360
- response = requests.post(webhook_url, json=payload, headers={"Content-Type": "application/json"}, timeout=15)
361
- if response.status_code == 200: return True
362
- else: print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}"); return False
363
- except Exception as e: print(f"Fel vid sändning till Slack: {e}"); return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
  # --- Feedback & Like-funktion ---
366
  def vote(data: gr.LikeData):
 
 
 
 
 
367
  feedback_type = "up" if data.liked else "down"
368
  global last_log, last_log_lock
369
- bot_reply_liked = data.value if isinstance(data.value, str) else (data.value.get("value") if isinstance(data.value, dict) else "[Kunde inte extrahera svar]")
370
-
371
- log_entry = {"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "event_type": "feedback", "feedback": feedback_type, "bot_reply_liked": bot_reply_liked}
372
- user_message, session_id, associated_bot_reply, user_id, ip_address, platform = "[Okänd fråga]", "unknown", "[Okänt svar]", "unknown", "unknown", "unknown"
373
-
 
 
 
 
 
 
 
 
374
  with last_log_lock:
375
- if last_log and last_log.get("event_type") == "conversation":
376
- user_message = last_log.get("user_message", user_message)
377
- session_id = last_log.get("session_id", session_id)
378
- associated_bot_reply = last_log.get("bot_reply", associated_bot_reply)
379
- user_id, ip_address, platform = last_log.get("user_id", user_id), last_log.get("ip", ip_address), last_log.get("platform", platform)
380
-
381
- log_entry.update({"session_id": session_id, "user_message": user_message, "associated_bot_reply": associated_bot_reply, "user_id": user_id, "ip": ip_address, "platform": platform})
 
382
  safe_append_to_log(log_entry)
383
-
384
- if feedback_type == "down":
385
- try:
386
- feedback_subject = f"👎 Negativ Feedback ({platform})"
387
- feedback_message = f"*Fråga:* {user_message[:500]}\n*Svar (som fick 👎):* {bot_reply_liked[:500]}\n---\n*Session:* `{session_id[:8]}` | *IP:* `{ip_address}` | *Tid:* {log_entry['timestamp']}"
388
- threading.Thread(target=send_to_slack, args=(feedback_subject, feedback_message.strip(), "#ff0000"), daemon=True).start()
389
- except Exception as e: print(f"Kunde inte starta tråd för neg. feedback: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  return
391
 
392
  # --- Rapportering ---
393
  def read_logs():
 
394
  logs = []
395
- if not os.path.exists(log_file_path): return logs
396
  try:
397
- with open(log_file_path, "r", encoding="utf-8", errors='replace') as file:
398
- malformed_lines = 0
399
- for line_count, line in enumerate(file, 1):
400
- line = line.strip()
401
- if not line: continue
402
- try: logs.append(json.loads(line))
403
- except json.JSONDecodeError: malformed_lines += 1
404
- if malformed_lines > 0: print(f"Varning: {malformed_lines} felaktigt formaterade rader i loggfilen.")
405
- except Exception as e: print(f"Fel vid läsning av loggfil: {e}")
 
 
 
 
 
 
 
406
  return logs
407
 
408
  def get_latest_conversations(logs, limit=50):
409
- # Implementering oförändrad...
410
  conversations = []
411
- count = 0
412
  for log in reversed(logs):
413
- if log.get('event_type') == 'conversation' and 'user_message' in log and 'bot_reply' in log:
414
  conversations.append({
415
  'user_message': log['user_message'],
416
  'bot_reply': log['bot_reply'],
417
  'timestamp': log.get('timestamp', '')
418
  })
419
- count += 1
420
- if count >= limit: break
421
  return conversations
422
 
423
  def get_feedback_stats(logs):
424
- # Implementering oförändrad...
425
  feedback_count = {"up": 0, "down": 0}
426
  negative_feedback_examples = []
 
427
  for log in logs:
428
- if log.get('event_type') == 'feedback' and 'feedback' in log:
429
  feedback = log.get('feedback')
430
- if feedback == "up": feedback_count["up"] += 1
431
- elif feedback == "down":
432
- feedback_count["down"] += 1
433
- if len(negative_feedback_examples) < 10:
434
- negative_feedback_examples.append({
435
- 'user_message': log.get('user_message', '[Okänd fråga]'),
436
- 'bot_reply_liked': log.get('bot_reply_liked', '[Okänt svar]')
437
- })
 
 
438
  return feedback_count, negative_feedback_examples
439
 
440
- def generate_periodic_stats(days=30):
441
- # Implementering oförändrad...
442
  print(f"Genererar statistik för de senaste {days} dagarna...")
443
- all_logs = read_logs(); now = datetime.now(); cutoff_date = now - timedelta(days=days)
444
- if not all_logs: return {"error": "Inga loggdata."}
445
- filtered_logs = [log for log in all_logs if (ts := log.get('timestamp')) and (log_date := _parse_datetime(ts)) and log_date >= cutoff_date]
446
- if not filtered_logs: return {"error": f"Inga loggar senaste {days} dagarna."}
447
-
448
- conv_logs = [log for log in filtered_logs if log.get('event_type') == 'conversation']
449
- feedback_logs = [log for log in filtered_logs if log.get('event_type') == 'feedback']
450
- total_conv = len(conv_logs)
451
- unique_sess = len(set(log.get('session_id') for log in conv_logs if log.get('session_id')))
452
- unique_users = len(set(log.get('user_id') for log in conv_logs if log.get('user_id')))
453
- fb_stats, neg_ex = get_feedback_stats(feedback_logs); pos_fb, neg_fb = fb_stats.get("up",0), fb_stats.get("down",0); total_fb = pos_fb + neg_fb
454
- fb_ratio = (pos_fb / total_fb * 100) if total_fb > 0 else 0
455
- resp_times = [t for log in conv_logs if isinstance(t := log.get('response_time'), (int, float))]
456
- avg_resp = sum(resp_times) / len(resp_times) if resp_times else 0
457
- plats, brows, osys = {}, {}, {}
458
- for log in conv_logs:
459
- p, b, o = log.get('platform','?'), log.get('browser','?'), log.get('os','?')
460
- plats[p], brows[b], osys[o] = plats.get(p,0)+1, brows.get(b,0)+1, osys.get(o,0)+1
461
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  report = {
463
- "period": f"{days} dagar ({cutoff_date.strftime('%Y-%m-%d')} - {now.strftime('%Y-%m-%d')})", "generated_at": now.strftime("%Y-%m-%d %H:%M:%S"),
464
- "basic_stats": {"conv": total_conv, "sess": unique_sess, "users": unique_users, "msg_p_sess": round(total_conv/unique_sess,2) if unique_sess else 0},
465
- "feedback": {"total": total_fb, "pos": pos_fb, "neg": neg_fb, "satisfaction": round(fb_ratio,1)},
466
- "performance": {"avg_resp_sec": round(avg_resp,2)},
467
- "top_platforms": dict(sorted(plats.items(), key=lambda i: i[1], reverse=True)[:5]),
468
- "top_browsers": dict(sorted(brows.items(), key=lambda i: i[1], reverse=True)[:5]),
469
- "top_os": dict(sorted(osys.items(), key=lambda i: i[1], reverse=True)[:5]),
470
- "neg_feedback_examples": neg_ex
 
 
 
 
 
 
 
 
 
 
 
471
  }
 
472
  return report
473
 
474
- def _parse_datetime(ts_str):
475
- try: return datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
476
- except (ValueError, TypeError): return None
477
-
478
- def format_report_for_slack(stats_dict):
479
- # Implementering oförändrad...
480
- if 'error' in stats_dict: return f"*Fel:* {stats_dict['error']}"
481
- b, f, p = stats_dict["basic_stats"], stats_dict["feedback"], stats_dict["performance"]
482
- plats, brows, osys, neg_ex = stats_dict["top_platforms"], stats_dict["top_browsers"], stats_dict["top_os"], stats_dict["neg_feedback_examples"]
483
- fmt_d = lambda d: "\n".join([f"- {k}: {v}" for k, v in d.items()]) if d else "_Ingen data_"
484
- fmt_neg = lambda exs: "\n".join([f"- `{ex['user_message'][:60]}`->`{ex['bot_reply_liked'][:60]}`" for ex in exs]) if exs else "_Inga_"
485
- return f"""*Period:* {stats_dict['period']} ({stats_dict['generated_at']})
486
- *📊 Anv:* Konv: {b['conv']}, Sess: {b['sess']}, Anv(est): {b['users']}, Med/Sess: {b['msg_p_sess']}
487
- *👍👎 Feedback:* Tot: {f['total']} ({f['pos']}👍/{f['neg']}👎), Nöjdhet: {f['satisfaction']}%
488
- *⚡ Prestanda:* Svarstid: {p['avg_resp_sec']}s
489
- *💻 Teknik:* Platf:\n{fmt_d(plats)}\nWebbläs:\n{fmt_d(brows)}\nOS:\n{fmt_d(osys)}
490
- *📉 Neg. Feedback Exempel:*\n{fmt_neg(neg_ex)}""".strip()
491
-
492
- def send_status_report(days=7, report_type="Daglig"):
493
- print(f"Genererar {report_type.lower()} rapport...")
494
- title = f"ChargeNode AI - {report_type} Status"
495
  try:
496
- stats = generate_periodic_stats(days=days)
497
- content = format_report_for_slack(stats)
498
- color = "#3498db" if "error" not in stats else "#ff0000"
499
- send_to_slack(title, content, color)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  except Exception as e:
501
- print(f"Fel vid {report_type.lower()} rapport: {e}")
502
- try: send_to_slack(f"🚨 Fel: {title}", f"*Fel:* `{type(e).__name__}: {e}`", "#ff0000")
503
- except: pass # Ignorera fel vid sändning av felmeddelande
 
 
 
504
 
505
- # --- Supportformulär till Slack ---
506
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
507
- # Implementering oförändrad...
508
- if not email: print("Fel: Email saknas."); return False
509
  try:
510
- hist_limit = 15; start_idx = max(0, len(chat_history) - hist_limit)
511
- chat_content = "\n\n".join([f"*{('👤 Anv' if m['role']=='user' else '🤖 Bot')} ({i+1}):* {m['content'][:400]}{'...' if len(m['content'])>400 else ''}"
512
- for i, m in enumerate(chat_history[start_idx:]) if isinstance(m, dict) and 'role' in m and 'content' in m])
513
- subject = f"📩 Support via Chatbot ({email})"
514
- ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
515
- content = f"*Ny supportförfrågan* ({ts})\n*Kontakt:* {email}\n*Områdeskod:* `{områdeskod or '?'}`\n*Uttag:* `{uttagsnummer or '?'}`\n---\n*Chatthistorik (senaste {min(hist_limit, len(chat_history))}):*\n{chat_content or '_Ingen historik_'}"
516
- return send_to_slack(subject, content.strip(), "#f4a261")
517
- except Exception as e: print(f"Fel vid sändning av support: {e}"); return False
518
-
519
- # --- Schemaläggning ---
520
- scheduler_running = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  def run_scheduler():
522
- global scheduler_running
523
- print("Startar schemaläggare...")
524
- schedule.every().day.at("08:00").do(lambda: send_status_report(days=1, report_type="Daglig"))
525
- schedule.every().monday.at("09:00").do(lambda: send_status_report(days=7, report_type="Veckovis"))
526
- schedule.every().day.at("10:00").do(monthly_report_if_first_day)
527
- print("Schema konfigurerat.")
528
- while scheduler_running:
529
- try:
530
- schedule.run_pending()
531
- for _ in range(60): # Check flag more often
532
- if not scheduler_running: break
533
- time.sleep(1)
534
- except Exception as e: print(f"Fel i schemaläggare: {e}"); time.sleep(300)
535
- print("Schemaläggare avslutas.")
536
-
537
- def monthly_report_if_first_day():
538
- if datetime.now().day == 1:
539
- print("Kör månadsrapport..."); send_status_report(days=30, report_type="Månadsvis")
540
-
541
- scheduler_thread = None
542
- if os.environ.get("SLACK_WEBHOOK_URL"):
543
- scheduler_thread = threading.Thread(target=run_scheduler, daemon=True); scheduler_thread.start()
544
- else: print("Info: Slack Webhook saknas, rapporter ej aktiva.")
545
-
546
- # --- Städning vid avslut ---
 
 
 
 
 
 
 
 
 
 
547
  def cleanup():
548
- global scheduler_running, scheduler_thread, commit_scheduler
549
  print("Städar upp resurser...")
550
- if scheduler_thread and scheduler_thread.is_alive():
551
- print("Stoppar schemaläggare..."); scheduler_running = False
552
- scheduler_thread.join(timeout=5)
553
- print("Schemaläggare stoppad." if not scheduler_thread.is_alive() else "Varning: Schemaläggare timeout.")
554
- if commit_scheduler:
555
- print("Försöker pusha sista loggarna till Hub...")
556
  try:
557
- # Kör i tråd för att undvika blockering vid exit
558
- threading.Thread(target=commit_scheduler.push_to_hub, daemon=False).start()
559
- time.sleep(5) # Ge lite tid att starta
560
- print("Sista push initierad (bakgrunden).")
561
- except Exception as e: print(f"Fel vid sista push: {e}")
562
- # Försök stänga filer (best effort)
563
  try:
 
564
  import gc
565
  for obj in gc.get_objects():
566
- if isinstance(obj, io.IOBase) and hasattr(obj, 'closed') and not obj.closed:
567
- try: obj.close()
568
- except: pass
569
- print("Försökt stänga filer.")
570
- except Exception as e: print(f"Fel vid filstängning: {e}")
 
 
 
571
  atexit.register(cleanup)
572
 
573
- # --- Inledande rapport ---
574
- def initial_startup_report():
575
- time.sleep(15); print("Skickar uppstartsrapport..."); send_status_report(days=1, report_type="Uppstart")
576
- if os.environ.get("SLACK_WEBHOOK_URL"): threading.Thread(target=initial_startup_report, daemon=True).start()
 
 
577
 
578
- # --- UI Funktioner ---
579
  def respond(message, chat_history, request: gr.Request):
580
  global last_log, last_log_lock
581
- if not message or not isinstance(message, str) or message.isspace(): return "", chat_history
582
- message = message.strip()[:1500]
583
- start_time = time.time()
584
- try: bot_response = generate_answer(message)
585
- except Exception as e: print(f"Fel vid generate_answer: {e}"); bot_response = f"Internt fel ({type(e).__name__}). Kontakta support."
586
- response_time = round(time.time() - start_time, 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
588
  session_id = str(uuid.uuid4())
 
 
589
  with last_log_lock:
590
- if last_log and last_log.get("event_type") == "conversation": session_id = last_log.get("session_id", session_id)
591
- user_id, ip_address, browser_info, os_info, platform, referer = "okänd", "okänd", "Okänd", "Okänd", "webb", ""
 
 
 
 
 
 
 
592
  if request:
593
  try:
594
- user_id = request.client.host if request.client else "?"
595
- hdrs = request.headers; ip_cand = [hdrs.get("x-forwarded-for"), hdrs.get("x-real-ip"), user_id]
596
- ip_address = next((ip.split(',')[0].strip() for ip in ip_cand if ip), user_id)
597
- ua_str = hdrs.get("user-agent",""); referer = hdrs.get("referer","")
598
- if ua_str: ua = parse_ua(ua_str); browser_info=f"{ua.browser.family} {ua.browser.version_string}" if ua.browser else "?"; os_info=f"{ua.os.family} {ua.os.version_string}" if ua.os else "?"
599
- if referer:
600
- if "chargenode.eu" in referer: platform = "chargenode.eu"
601
- elif any(s in referer for s in ["localhost", "127.0."]): platform = "test"
602
- elif "app" in referer: platform = "app"
603
- else: platform = "direkt/?"
604
- except Exception as req_err: print(f"Fel vid req metadata: {req_err}")
605
-
606
- log_data = {"timestamp": timestamp, "event_type": "conversation", "user_id": user_id, "session_id": session_id, "ip": ip_address, "platform": platform, "browser": browser_info, "os": os_info, "user_message": message, "bot_reply": bot_response, "response_time": response_time}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  safe_append_to_log(log_data)
608
- with last_log_lock: last_log = log_data
 
 
 
609
 
 
610
  try:
611
- slack_subj = f"💬 Ny konv ({platform})"
612
- slack_cont = f"*Anv:* {message}\n*Bot:* {bot_response[:350]}{'...' if len(bot_response)>350 else ''}\n---\n*Info:* `{timestamp}` | `{session_id[:8]}` | `{ip_address}` | `{browser_info}` | `{os_info}`"
613
- threading.Thread(target=send_to_slack, args=(slack_subj, slack_cont.strip(), "#ADD8E6"), daemon=True).start()
614
- except Exception as e: print(f"Kunde inte starta Slack-tråd: {e}")
 
 
 
615
 
616
- chat_history.append({"role": "user", "content": message})
617
- chat_history.append({"role": "assistant", "content": bot_response})
618
- return "", chat_history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
  def format_chat_preview(chat_history):
621
- # Implementering oförändrad...
622
- if not chat_history: return "_Ingen historik_"
623
- hist_limit = 15; start_idx = max(0, len(chat_history) - hist_limit)
624
- return "\n\n".join([f"**{'Du' if m['role']=='user' else 'Bot'}:** {m['content'][:150]}{'...' if len(m['content'])>150 else ''}"
625
- for m in chat_history[start_idx:] if isinstance(m, dict) and 'role' in m and 'content' in m]).strip()
 
 
 
 
 
 
 
626
 
627
  def show_support_form(chat_history):
628
- preview_md = format_chat_preview(chat_history)
629
- return {chat_interface: gr.update(visible=False), support_interface: gr.update(visible=True), success_interface: gr.update(visible=False), chat_preview: gr.update(value=preview_md)}
 
 
 
 
 
630
 
631
  def back_to_chat():
632
- return {chat_interface: gr.update(visible=True), support_interface: gr.update(visible=False), success_interface: gr.update(visible=False)}
 
 
 
 
633
 
634
  def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
635
- # Implementering oförändrad...
636
- errors = []
637
- if områdeskod and not områdeskod.isdigit(): errors.append("Områdeskod?")
638
- if uttagsnummer and not uttagsnummer.isdigit(): errors.append("Uttagsnummer?")
639
- if not email or '@' not in email or '.' not in email.split('@')[-1]: errors.append("E-post?")
640
- if errors:
641
- msg = "**Valideringsfel:**\n- " + "\n- ".join(errors)
642
- return {chat_interface: gr.update(visible=False), support_interface: gr.update(visible=True), success_interface: gr.update(visible=False), chat_preview: gr.update(value=msg)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  try:
 
 
 
 
 
 
 
 
 
 
644
  success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
645
- if success: return {chat_interface: gr.update(visible=False), support_interface: gr.update(visible=False), success_interface: gr.update(visible=True), chat_preview: gr.update(value="")}
646
- else: msg = "**Fel:** Meddelandet kunde inte skickas. Försök igen eller maila support@chargenode.eu."
647
- except Exception as e: msg = f"**Oväntat fel:** {e}"
648
- return {chat_interface: gr.update(visible=False), support_interface: gr.update(visible=True), success_interface: gr.update(visible=False), chat_preview: gr.update(value=msg)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
 
650
- # --- Gradio UI Definition ---
651
- initial_chat = [{"role": "assistant", "content": "Hej! Jag är ChargeNodes AI-assistent. Ställ din fråga nedan."}]
652
 
653
  custom_css = """
654
- body { font-family: sans-serif; margin: 0; padding: 0; background-color: #f8f9fa; }
655
- .gradio-container { max-width: 420px !important; margin: 10px auto; box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-radius: 10px; background-color: #ffffff; overflow: hidden; border: 1px solid #dee2e6; position: fixed; bottom: 15px; right: 15px; }
656
- h1 { font-family: 'Segoe UI', sans-serif; color: #1e88e5; text-align: center; margin: 0.8em 0 0.6em 0; font-size: 1.4em; font-weight: 600; }
657
- #chatbot_conversation .message-wrap { padding: 12px; }
658
- #chatbot_conversation { min-height: 280px; max-height: 350px; overflow-y: auto; border-bottom: 1px solid #e9ecef; background-color: #f8f9fa;}
659
- .message { padding: 8px 12px; margin-bottom: 6px; border-radius: 15px; max-width: 88%; word-wrap: break-word; line-height: 1.4; font-size: 0.95em;}
660
- .message.user { background-color: #e3f2fd; border-radius: 15px 15px 5px 15px; margin-left: auto; }
661
- .message.bot { background-color: #e9ecef; border-radius: 15px 15px 15px 5px; margin-right: auto; }
662
- .support-form-container, .success-interface { padding: 15px 20px; }
663
- .support-form-container .gr-form { margin-top: 10px; padding: 15px; border: 1px solid #ced4da; border-radius: 8px; background-color: #f1f3f5; }
664
- .gr-button { background-color: #1e88e5; color: white; border: none; border-radius: 5px; padding: 9px 16px; margin: 4px; cursor: pointer; transition: background-color 0.2s ease; font-weight: 500; font-size: 0.9em; }
665
- .gr-button:hover { background-color: #1565c0; }
666
- .support-btn { background-color: #6c757d; } .support-btn:hover { background-color: #5a6268; }
667
- .clear-btn { background-color: #dc3545; } .clear-btn:hover { background-color: #c82333; }
668
- .flex-row { display: flex; flex-direction: row; gap: 8px; align-items: center; }
669
- .chat-preview { max-height: 140px; overflow-y: auto; border: 1px solid #ced4da; padding: 10px; margin-top: 12px; font-size: 0.85em; background-color: #ffffff; border-radius: 4px; line-height: 1.3; }
670
- .success-message { font-size: 1.05em; font-weight: 500; color: #28a745; margin-bottom: 15px; text-align: center; }
671
- footer { display: none !important; }
672
- .gradio-container footer, .gradio-container .gr-footer { display: none !important; visibility: hidden !important; }
673
- .gr-textbox textarea { border-radius: 5px; border: 1px solid #ced4da; padding: 10px; font-size: 0.95em;}
674
- .block.padded { padding: 10px 15px !important; }
675
- .block.gap { gap: 8px !important; }
 
676
  """
677
 
 
678
  with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
679
- gr.Markdown("# ChargeNode AI Assistent")
680
-
 
681
  with gr.Group(visible=True) as chat_interface:
682
- chatbot = gr.Chatbot(
683
- value=initial_chat,
684
- type='messages', # <<< KORRIGERAD HÄR
685
- elem_id="chatbot_conversation",
686
- label="Chatt", show_label=False
687
- # bubble_full_width är borttagen
688
- )
689
- chatbot.like(vote, inputs=None, outputs=None)
690
-
691
- with gr.Row(equal_height=False):
692
- msg = gr.Textbox(label="Meddelande", placeholder="Skriv din fråga här...", show_label=False, scale=4, lines=1, max_lines=3)
693
- # submit_btn = gr.Button("Skicka", scale=1, min_width=80) # Ta bort om Enter räcker
694
-
695
  with gr.Row():
696
- clear_btn = gr.Button("Rensa chatt", elem_classes="clear-btn")
697
- support_btn = gr.Button("Kontakta support", elem_classes="support-btn")
698
-
699
- with gr.Group(visible=False, elem_classes="support-form-container") as support_interface:
700
- gr.Markdown("### Kontakta Support")
701
- gr.Markdown("Fyll i dina uppgifter så återkommer vi via email. Din chatthistorik inkluderas.")
702
- with gr.Column(elem_classes="gr-form"):
703
- email = gr.Textbox(label="Din E-postadress", placeholder="din.email@example.com", info="Obligatorisk.")
704
- områdeskod = gr.Textbox(label="Områdeskod (frivilligt)", placeholder="t.ex. 12345")
705
- uttagsnummer = gr.Textbox(label="Uttagsnummer (frivilligt)", placeholder="t.ex. 1")
706
- gr.Markdown("#### Chatthistorik (senaste)")
707
- chat_preview = gr.Markdown(value="_Laddar..._", elem_classes="chat-preview")
708
- with gr.Row():
709
- back_btn = gr.Button("Avbryt")
710
- send_support_btn = gr.Button("Skicka till Support")
711
-
712
- with gr.Group(visible=False) as success_interface:
713
- gr.Markdown("✅ Tack! Din förfrågan har skickats.", elem_classes="success-message")
714
- back_to_chat_btn = gr.Button("Tillbaka till chatten")
715
-
716
- # --- JavaScript för Scrollning ---
717
  js_code = """
718
- function scrollToTopOfLatestBotMessage() {
719
- const selectorOptions = [
720
- '.gradio-container .message-wrap .message.bot:last-child',
721
- '.gradio-container [data-testid="bot"]:last-child',
722
- '.gradio-container #chatbot_conversation .message:is(.bot, [data-testid="bot"]):last-of-type'
723
- ];
724
- let latestBotMessage = null;
725
- for (const selector of selectorOptions) {
726
- latestBotMessage = document.querySelector(selector);
727
- if (latestBotMessage) break;
728
- }
729
- if (latestBotMessage) {
730
- try { latestBotMessage.scrollIntoView({ block: 'start', behavior: 'smooth' }); }
731
- catch (e) {
732
- console.error("Scroll Error:", e);
733
- const chatContainer = latestBotMessage.closest('#chatbot_conversation .message-wrap');
734
- if (chatContainer) { try { chatContainer.scrollTop = latestBotMessage.offsetTop - chatContainer.offsetTop; } catch (e2) {} }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
  }
737
  }
738
- function setupObserver() {
739
- const chatRoot = document.querySelector('#chatbot_conversation');
740
- if (!chatRoot) { setTimeout(setupObserver, 300); return; }
741
- const observer = new MutationObserver((mutationsList) => {
742
- let botMessageAdded = false;
743
- for (const mutation of mutationsList) {
744
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
745
- mutation.addedNodes.forEach(node => {
746
- if (node.nodeType === Node.ELEMENT_NODE && (node.matches('.message.bot') || node.matches('[data-testid="bot"]'))) { botMessageAdded = true; }
747
- else if (node.nodeType === Node.ELEMENT_NODE && node.querySelector && (node.querySelector('.message.bot') || node.querySelector('[data-testid="bot"]'))) { botMessageAdded = true; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  });
749
  }
750
- if (botMessageAdded) break;
 
 
 
 
 
 
 
 
 
751
  }
752
- if (botMessageAdded) { setTimeout(scrollToTopOfLatestBotMessage, 200); }
753
  });
754
- observer.observe(chatRoot, { childList: true, subtree: true });
755
- }
756
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupObserver); } else { setupObserver(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  """
758
  app.load(js=js_code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
 
760
- # --- Event Listeners ---
761
- msg.submit(fn=respond, inputs=[msg, chatbot], outputs=[msg, chatbot], api_name="send_message")
762
- # submit_btn.click(fn=respond, inputs=[msg, chatbot], outputs=[msg, chatbot], api_name="send_message") # Om knappen används
763
- clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg], queue=False)
764
- support_btn.click(fn=show_support_form, inputs=[chatbot], outputs=[chat_interface, support_interface, success_interface, chat_preview])
765
- back_btn.click(fn=back_to_chat, inputs=None, outputs=[chat_interface, support_interface, success_interface])
766
- back_to_chat_btn.click(fn=back_to_chat, inputs=None, outputs=[chat_interface, support_interface, success_interface])
767
- send_support_btn.click(fn=submit_support_form, inputs=[områdeskod, uttagsnummer, email, chatbot], outputs=[chat_interface, support_interface, success_interface, chat_preview])
768
-
769
- # --- Kör appen ---
770
  if __name__ == "__main__":
771
- print("Förbereder start...")
772
- try:
773
- initialize_embeddings() # Ladda embeddings FÖRE launch
774
- print("Embeddings klara.")
775
- should_share = os.environ.get("GRADIO_SHARE", "false").lower() == "true"
776
- print(f"Startar Gradio app {'MED' if should_share else 'UTAN'} delning...")
777
- app.launch(share=should_share)
778
- except RuntimeError as e: print(f"Kunde inte starta appen p.g.a. tidigare fel: {e}")
779
- except Exception as e: print(f"Oväntat fel vid Gradio start: {e}")
 
 
1
  import os
2
  import json
3
  import time
 
36
 
37
  # Skapa en tom loggfil om den inte finns
38
  if not os.path.exists(log_file_path):
39
+ with open(log_file_path, "w", encoding="utf-8") as f:
40
+ f.write("") # Skapa en tom fil
41
+ print(f"Skapade tom loggfil: {log_file_path}")
 
 
 
 
 
 
42
 
43
  hf_token = os.environ.get("HF_TOKEN")
44
+ if not hf_token:
45
+ raise ValueError("HF_TOKEN saknas")
 
46
 
47
+ # Minsta möjliga konfiguration som bör fungera
48
+ scheduler = CommitScheduler(
49
+ repo_id="ChargeNodeEurope/logfiles",
50
+ repo_type="dataset",
51
+ folder_path=log_folder,
52
+ path_in_repo="logs_v2",
53
+ every=300, # Vänta 5 minuter
54
+ token=hf_token
55
+ )
 
 
 
 
 
 
 
 
 
56
 
57
 
58
  # --- Globala variabler ---
59
+ last_log = None # Sparar loggdata från senaste svar för feedback
60
+ # Lägg till en lock för att skydda åtkomst till last_log
61
  last_log_lock = threading.Lock()
62
 
63
  # --- Förbättrad loggfunktion ---
64
  def safe_append_to_log(log_entry):
65
+ """Säker metod för att lägga till loggdata utan att förlora historisk information."""
66
  try:
67
+ # Kontrollera att loggmappen finns
68
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
69
+
70
+ # Kontrollera loggfilens storlek
71
  try:
72
+ log_size = os.path.getsize(log_file_path)
73
+ # Om loggfilen är större än 10MB, rotera den
74
+ if log_size > 10 * 1024 * 1024: # 10MB
75
+ backup_path = f"{log_file_path}.bak"
76
+ if os.path.exists(backup_path):
77
+ os.remove(backup_path)
78
+ os.rename(log_file_path, backup_path)
79
+ print(f"Loggfil roterad: {log_file_path} -> {backup_path}")
80
+ except FileNotFoundError:
81
+ # Filen finns inte än, inget att rotera
82
+ pass
83
+
84
+ # Sanitera loggdata för att undvika potentiella injektioner
85
+ sanitized_entry = {}
86
+ for k, v in log_entry.items():
87
+ if isinstance(v, str):
88
+ # Ta bort null-bytes och begränsa längden
89
+ sanitized_entry[k] = v.replace('\0', '')[:10000] # Begränsa till 10000 tecken
90
+ else:
91
+ sanitized_entry[k] = v
92
+
93
+ # Öppna filen i append-läge
94
  with open(log_file_path, "a", encoding="utf-8") as log_file:
95
+ log_json = json.dumps(sanitized_entry)
96
  log_file.write(log_json + "\n")
97
+ log_file.flush() # Säkerställ att data skrivs till disk omedelbart
98
+
99
+ print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}")
100
  return True
101
+
102
  except Exception as e:
103
+ print(f"Fel vid loggning: {e}")
104
+
105
+ # Försök skapa mappen om den inte finns (detta bör redan ha gjorts ovan)
106
  try:
107
+ os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
108
+
109
+ # Försök igen med minimal loggdata för att undvika fel
110
+ minimal_entry = {
111
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
112
+ "error": f"Fel vid loggning: {str(e)[:200]}",
113
+ "recovery": True
114
+ }
115
+
116
+ with open(log_file_path, "a", encoding="utf-8") as log_file:
117
+ log_json = json.dumps(minimal_entry)
118
+ log_file.write(log_json + "\n")
119
+
120
+ print("Minimal loggpost tillagd efter återhämtning")
121
+ return True
122
+
123
+ except Exception as retry_error:
124
+ print(f"Kritiskt fel vid loggning: {retry_error}")
125
+ return False
126
 
127
  # --- Laddar textkällor ---
128
  def load_local_files():
129
  uploaded_text = ""
130
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
131
+ excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets"]
132
+ for file in os.listdir("."):
133
+ if file.lower().endswith(tuple(allowed)) and file not in excluded:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  try:
135
+ if file.endswith(".txt"):
136
+ with open(file, "r", encoding="utf-8") as f:
137
+ content = f.read()
138
+ elif file.endswith(".docx"):
139
+ from docx import Document # Import sker vid behov
140
+ content = "\n".join([p.text for p in Document(file).paragraphs])
141
+ elif file.endswith(".pdf"):
142
+ import PyPDF2 # Import sker vid behov
143
+ with open(file, "rb") as f:
144
+ reader = PyPDF2.PdfReader(f)
145
+ content = "\n".join([p.extract_text() or "" for p in reader.pages])
146
+ elif file.endswith(".csv"):
147
+ content = pd.read_csv(file).to_string()
148
+ elif file.endswith((".xls", ".xlsx")):
149
+ if file == "FAQ stadat.xlsx":
150
+ df = pd.read_excel(file)
151
+ rows = []
152
+ for index, row in df.iterrows():
153
+ rows.append(f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}")
154
+ content = "\n\n".join(rows)
155
+ else:
156
+ content = pd.read_excel(file).to_string()
157
+ uploaded_text += f"\n\nFIL: {file}\n{content}"
158
+ except Exception as e:
159
+ print(f"Fel vid läsning av {file}: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  return uploaded_text.strip()
161
 
162
  def load_prompt():
 
163
  try:
164
+ with open("prompt.txt", "r", encoding="utf-8") as f:
165
+ return f.read().strip()
 
 
166
  except Exception as e:
167
+ print(f"Fel vid prompt.txt: {e}")
168
+ return ""
169
 
170
  prompt_template = load_prompt()
171
 
172
  # Förbered textsegment
173
  def prepare_chunks(text_data):
174
+ chunks, sources = [], []
 
175
  for source, text in text_data.items():
176
+ paragraphs = [p for p in text.split("\n") if p.strip()]
177
+ chunk = ""
 
 
 
 
 
 
178
  for para in paragraphs:
179
+ if len(chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
180
+ chunk += " " + para
 
 
 
 
 
 
 
 
181
  else:
182
+ if chunk.strip():
183
+ chunks.append(chunk.strip())
184
+ sources.append(source)
185
+ chunk = para
186
+ if chunk.strip():
187
+ chunks.append(chunk.strip())
188
+ sources.append(source)
189
+ return chunks, sources
190
+
191
+ # Lazy-laddning av SentenceTransformer
192
+ embedder = None
193
+ embeddings = None
194
+ index = None
195
+ chunks = []
196
+ chunk_sources = []
197
  embedding_lock = threading.Lock()
198
 
199
  def initialize_embeddings():
200
+ """Initierar SentenceTransformer och FAISS-index vid första anrop."""
201
  global embedder, embeddings, index, chunks, chunk_sources
202
+
203
+ # Använd en lock för att förhindra att flera trådar initierar samtidigt
204
  with embedding_lock:
205
+ if embedder is None:
206
+ try:
207
+ print("Initierar SentenceTransformer och FAISS-index...")
208
+ # Ladda och förbered lokal data
209
+ print("Laddar textdata...")
210
+ text_data = {"local_files": load_local_files()}
211
+ print("Förbereder textsegment...")
212
+ chunks, chunk_sources = prepare_chunks(text_data)
213
+ print(f"{len(chunks)} segment laddade")
214
+
215
+ if not chunks:
216
+ print("Varning: Inga textsegment hittades. Kontrollera textdata.")
217
+ # Skapa tomma listor för att undvika fel
218
+ chunks = [""]
219
+ chunk_sources = ["empty"]
220
+
221
+ print("Skapar embeddings...")
222
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
223
+ embeddings = embedder.encode(chunks, convert_to_numpy=True)
224
+
225
+ # Kontrollera att embeddings inte är tomma
226
+ if embeddings.size > 0:
227
+ embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
228
+ index = faiss.IndexFlatIP(embeddings.shape[1])
229
+ index.add(embeddings)
230
+ print("FAISS-index klart")
231
+ else:
232
+ print("Varning: Tomma embeddings. Skapar ett tomt index.")
233
+ # Skapa ett tomt index med rätt dimensioner
234
+ index = faiss.IndexFlatIP(384) # Standard dimension för all-MiniLM-L6-v2
235
+ except Exception as e:
236
+ print(f"Fel vid initiering av embeddings: {e}")
237
+ # Sätt upp grundläggande värden för att undvika fel
238
+ if embedder is None:
239
+ print("Försöker återhämta från fel...")
240
+ try:
241
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
242
+ if not chunks:
243
+ chunks = ["Fel vid laddning av data"]
244
+ chunk_sources = ["error"]
245
+ embeddings = embedder.encode(chunks, convert_to_numpy=True)
246
+ index = faiss.IndexFlatIP(embeddings.shape[1])
247
+ index.add(embeddings)
248
+ print("Återhämtning lyckades")
249
+ except Exception as recovery_error:
250
+ print(f"Kunde inte återhämta: {recovery_error}")
251
+ # Sätt upp dummy-värden som sista utväg
252
+ embedder = None
253
+ embeddings = np.zeros((1, 384))
254
+ index = faiss.IndexFlatIP(384)
255
  def retrieve_context(query, k=RETRIEVAL_K):
256
+ """Hämtar relevant kontext för frågor."""
257
+ # Säkerställ att modeller är laddade
258
+ initialize_embeddings()
259
+
 
260
  try:
261
+ if embedder is None:
262
+ print("Varning: Embedder är fortfarande None efter initiering")
263
+ return "Kunde inte ladda kontext", ["error"]
264
+
265
  query_embedding = embedder.encode([query], convert_to_numpy=True)
266
+
267
+ # Kontrollera att embedding inte är tom
268
+ if query_embedding.size == 0:
269
+ print("Varning: Tom query embedding")
270
+ return "Kunde inte bearbeta frågan", ["error"]
271
+
272
+ query_embedding /= np.linalg.norm(query_embedding)
273
+
274
+ # Kontrollera att index finns
275
+ if index is None:
276
+ print("Varning: FAISS-index är None")
277
+ return "Kunde inte söka i kontext", ["error"]
278
+
279
+ # Säkerställ att k inte är större än antalet element i index
280
  actual_k = min(k, index.ntotal)
281
+ if actual_k < k:
282
+ print(f"Varning: Justerade k från {k} till {actual_k} baserat på index.ntotal")
283
+
284
  D, I = index.search(query_embedding, actual_k)
285
+
286
+ retrieved, sources = [], set()
287
+ for idx in I[0]:
288
+ if 0 <= idx < len(chunks):
289
+ retrieved.append(chunks[idx])
290
+ sources.add(chunk_sources[idx])
291
+
292
+ if not retrieved:
293
+ print("Varning: Ingen relevant kontext hittades")
294
+ return "Ingen relevant kontext hittades", ["no_context"]
295
+
296
+ return " ".join(retrieved), list(sources)
297
  except Exception as e:
298
  print(f"Fel vid hämtning av kontext: {e}")
299
+ return f"Fel vid kontexthämtning: {str(e)[:200]}", ["error"]
300
 
301
  def generate_answer(query):
302
+ """Genererar svar baserat på fråga och kontextinformation."""
303
  context, sources = retrieve_context(query)
304
+ if not context.strip():
305
+ return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI genererat svar."
306
+ prompt = f"""{prompt_template}
307
+
308
+ Relevant kontext:
309
+ {context}
310
+ Fråga: {query}
311
+ Svar (baserat enbart på den indexerade datan):"""
312
  try:
313
  response = client.chat.completions.create(
314
  model="gpt-3.5-turbo",
315
+ messages=[
316
+ {"role": "system", "content": "Du är en expert på ChargeNodes produkter och tjänster. Svara enbart baserat på den information som finns i den indexerade datan."},
317
+ {"role": "user", "content": prompt}
318
+ ],
319
+ temperature=0.2,
320
+ max_tokens=500
321
  )
322
+ answer = response.choices[0].message.content
323
+ return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
 
324
  except Exception as e:
325
+ return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
 
326
 
327
  # --- Slack Integration ---
328
  def send_to_slack(subject, content, color="#2a9d8f"):
329
+ """Basfunktion för att skicka meddelanden till Slack."""
330
  webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
331
+ if not webhook_url:
332
+ print("Slack webhook URL saknas")
333
+ return False
334
+
335
  try:
336
+ # Formatera meddelandet för Slack
337
+ payload = {
338
+ "blocks": [
339
+ {
340
+ "type": "header",
341
+ "text": {
342
+ "type": "plain_text",
343
+ "text": subject
344
+ }
345
+ },
346
+ {
347
+ "type": "section",
348
+ "text": {
349
+ "type": "mrkdwn",
350
+ "text": content
351
+ }
352
+ }
353
+ ]
354
+ }
355
+
356
+ response = requests.post(
357
+ webhook_url,
358
+ json=payload,
359
+ headers={"Content-Type": "application/json"}
360
+ )
361
+
362
+ if response.status_code == 200:
363
+ print(f"Slack-meddelande skickat: {subject}")
364
+ return True
365
+ else:
366
+ print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
367
+ return False
368
+ except Exception as e:
369
+ print(f"Fel vid sändning till Slack: {type(e).__name__}: {e}")
370
+ return False
371
 
372
  # --- Feedback & Like-funktion ---
373
  def vote(data: gr.LikeData):
374
+ """
375
+ Hanterar feedback från Gradio's inbyggda like-funktion.
376
+ data.liked är True om uppvote, annars False.
377
+ data.value innehåller information om meddelandet.
378
+ """
379
  feedback_type = "up" if data.liked else "down"
380
  global last_log, last_log_lock
381
+
382
+ # Skapa en kopia av data.value för att undvika potentiella race conditions
383
+ bot_reply = data.value if not isinstance(data.value, dict) else data.value.get("value", "")
384
+ if bot_reply is None:
385
+ bot_reply = ""
386
+
387
+ log_entry = {
388
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
389
+ "feedback": feedback_type,
390
+ "bot_reply": bot_reply
391
+ }
392
+
393
+ # Använd lock för att säkert komma åt last_log
394
  with last_log_lock:
395
+ # Om global logdata finns, lägg till ytterligare metadata.
396
+ if last_log:
397
+ log_entry.update({
398
+ "session_id": last_log.get("session_id", "unknown"),
399
+ "user_message": last_log.get("user_message", "Okänd fråga"),
400
+ })
401
+
402
+ # Använd den förbättrade loggfunktionen
403
  safe_append_to_log(log_entry)
404
+
405
+ # Skicka feedback till Slack
406
+ try:
407
+ if feedback_type == "down": # Skicka bara negativ feedback
408
+ # Hämta user_message säkert
409
+ user_message = "Okänd fråga"
410
+ with last_log_lock:
411
+ if last_log:
412
+ user_message = last_log.get("user_message", "Okänd fråga")
413
+
414
+ feedback_message = f"""
415
+ *⚠️ Negativ feedback registrerad*
416
+
417
+ *Fråga:* {user_message}
418
+
419
+ *Svar:* {log_entry.get('bot_reply', 'Okänt svar')[:300]}{'...' if len(log_entry.get('bot_reply', '')) > 300 else ''}
420
+ """
421
+ # Skicka asynkront
422
+ threading.Thread(
423
+ target=lambda: send_to_slack("Negativ feedback", feedback_message, "#ff0000"),
424
+ daemon=True
425
+ ).start()
426
+ except Exception as e:
427
+ print(f"Kunde inte skicka feedback till Slack: {e}")
428
+
429
  return
430
 
431
  # --- Rapportering ---
432
  def read_logs():
433
+ """Läs alla loggposter från loggfilen."""
434
  logs = []
 
435
  try:
436
+ if os.path.exists(log_file_path):
437
+ with open(log_file_path, "r", encoding="utf-8") as file:
438
+ line_count = 0
439
+ for line in file:
440
+ line_count += 1
441
+ try:
442
+ log_entry = json.loads(line.strip())
443
+ logs.append(log_entry)
444
+ except json.JSONDecodeError as e:
445
+ print(f"Varning: Kunde inte tolka rad {line_count}: {e}")
446
+ continue
447
+ print(f"Läste {len(logs)} av {line_count} loggposter")
448
+ else:
449
+ print(f"Loggfil saknas: {log_file_path}")
450
+ except Exception as e:
451
+ print(f"Fel vid läsning av loggfil: {e}")
452
  return logs
453
 
454
  def get_latest_conversations(logs, limit=50):
455
+ """Hämta de senaste frågorna och svaren."""
456
  conversations = []
 
457
  for log in reversed(logs):
458
+ if 'user_message' in log and 'bot_reply' in log:
459
  conversations.append({
460
  'user_message': log['user_message'],
461
  'bot_reply': log['bot_reply'],
462
  'timestamp': log.get('timestamp', '')
463
  })
464
+ if len(conversations) >= limit:
465
+ break
466
  return conversations
467
 
468
  def get_feedback_stats(logs):
469
+ """Sammanfatta feedback (tumme upp/ned)."""
470
  feedback_count = {"up": 0, "down": 0}
471
  negative_feedback_examples = []
472
+
473
  for log in logs:
474
+ if 'feedback' in log:
475
  feedback = log.get('feedback')
476
+ if feedback in feedback_count:
477
+ feedback_count[feedback] += 1
478
+
479
+ # Samla exempel på negativ feedback
480
+ if feedback == "down" and 'user_message' in log and len(negative_feedback_examples) < 10:
481
+ negative_feedback_examples.append({
482
+ 'user_message': log.get('user_message', 'Okänd fråga'),
483
+ 'bot_reply': log.get('bot_reply', 'Okänt svar')
484
+ })
485
+
486
  return feedback_count, negative_feedback_examples
487
 
488
+ def generate_monthly_stats(days=30):
489
+ """Genererar omfattande statistik över botanvändning för den senaste månaden."""
490
  print(f"Genererar statistik för de senaste {days} dagarna...")
491
+
492
+ # Hämta loggar
493
+ logs = read_logs()
494
+
495
+ if not logs:
496
+ return {"error": "Inga loggar hittades för den angivna perioden"}
497
+
498
+ # Filtrera på datumintervall
499
+ now = datetime.now()
500
+ cutoff_date = now - timedelta(days=days)
501
+ filtered_logs = []
502
+
503
+ for log in logs:
504
+ if 'timestamp' in log:
505
+ try:
506
+ log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S")
507
+ if log_date >= cutoff_date:
508
+ filtered_logs.append(log)
509
+ except:
510
+ pass # Hoppa över poster med ogiltigt datum
511
+
512
+ logs = filtered_logs
513
+
514
+ # Basstatistik
515
+ total_conversations = sum(1 for log in logs if 'user_message' in log)
516
+ unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
517
+ unique_users = len(set(log.get('user_id', 'unknown') for log in logs if 'user_id' in log))
518
+
519
+ # Feedback-statistik
520
+ feedback_logs = [log for log in logs if 'feedback' in log]
521
+ positive_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'up')
522
+ negative_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'down')
523
+ feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
524
+
525
+ # Svarstidsstatistik
526
+ response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log]
527
+ avg_response_time = sum(response_times) / len(response_times) if response_times else 0
528
+
529
+ # Plattformsstatistik
530
+ platforms = {}
531
+ browsers = {}
532
+ operating_systems = {}
533
+ for log in logs:
534
+ if 'platform' in log:
535
+ platforms[log['platform']] = platforms.get(log['platform'], 0) + 1
536
+ if 'browser' in log:
537
+ browsers[log['browser']] = browsers.get(log['browser'], 0) + 1
538
+ if 'os' in log:
539
+ operating_systems[log['os']] = operating_systems.get(log['os'], 0) + 1
540
+
541
+ # Skapa rapport
542
  report = {
543
+ "period": f"Senaste {days} dagarna",
544
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
545
+ "basic_stats": {
546
+ "total_conversations": total_conversations,
547
+ "unique_sessions": unique_sessions,
548
+ "unique_users": unique_users,
549
+ "messages_per_user": round(total_conversations / unique_users, 2) if unique_users else 0
550
+ },
551
+ "feedback": {
552
+ "positive": positive_feedback,
553
+ "negative": negative_feedback,
554
+ "ratio_percent": round(feedback_ratio, 1)
555
+ },
556
+ "performance": {
557
+ "avg_response_time": round(avg_response_time, 2)
558
+ },
559
+ "platform_distribution": platforms,
560
+ "browser_distribution": browsers,
561
+ "os_distribution": operating_systems
562
  }
563
+
564
  return report
565
 
566
+ def simple_status_report():
567
+ """Skickar en förenklad statusrapport till Slack."""
568
+ print("Genererar statusrapport för Slack...")
569
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  try:
571
+ # Generera statistik
572
+ stats = generate_monthly_stats(days=7) # Senaste veckan
573
+
574
+ # Skapa innehåll för Slack
575
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
576
+ subject = f"ChargeNode AI Bot - Status {now}"
577
+
578
+ if 'error' in stats:
579
+ content = f"*Fel vid generering av statistik:* {stats['error']}"
580
+ return send_to_slack(subject, content, "#ff0000")
581
+
582
+ # Formatera statistik
583
+ basic = stats["basic_stats"]
584
+ feedback = stats["feedback"]
585
+ perf = stats["performance"]
586
+
587
+ content = f"""
588
+ *ChargeNode AI Bot - Statusrapport {now}*
589
+
590
+ *Basstatistik* (senaste 7 dagarna)
591
+ - Totalt antal konversationer: {basic['total_conversations']}
592
+ - Unika sessioner: {basic['unique_sessions']}
593
+ - Unika användare: {basic['unique_users']}
594
+ - Genomsnittlig svarstid: {perf['avg_response_time']} sekunder
595
+
596
+ *Feedback*
597
+ - 👍 Tumme upp: {feedback['positive']}
598
+ - 👎 Tumme ned: {feedback['negative']}
599
+ - Nöjdhet: {feedback['ratio_percent']}%
600
+ """
601
+
602
+ # Lägg till de senaste konversationerna
603
+ logs = read_logs()
604
+ conversations = get_latest_conversations(logs, 3)
605
+
606
+ if conversations:
607
+ content += "\n*Senaste konversationer*\n"
608
+ for conv in conversations:
609
+ content += f"""
610
+ > *Tid:* {conv['timestamp']}
611
+ > *Fråga:* {conv['user_message'][:100]}{'...' if len(conv['user_message']) > 100 else ''}
612
+ > *Svar:* {conv['bot_reply'][:100]}{'...' if len(conv['bot_reply']) > 100 else ''}
613
+ """
614
+
615
+ # Skicka till Slack
616
+ return send_to_slack(subject, content, "#2a9d8f")
617
+
618
  except Exception as e:
619
+ print(f"Fel vid generering av statusrapport: {e}")
620
+
621
+ # Skicka felmeddelande till Slack
622
+ error_subject = f"ChargeNode AI Bot - Fel vid statusrapport"
623
+ error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
624
+ return send_to_slack(error_subject, error_content, "#ff0000")
625
 
 
626
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
627
+ """Skickar en supportförfrågan till Slack."""
 
628
  try:
629
+ # Formatera chat-historiken
630
+ chat_content = ""
631
+ for msg in chat_history:
632
+ if msg['role'] == 'user':
633
+ chat_content += f">*Användare:* {msg['content']}\n\n"
634
+ elif msg['role'] == 'assistant':
635
+ chat_content += f">*Bot:* {msg['content'][:300]}{'...' if len(msg['content']) > 300 else ''}\n\n"
636
+
637
+ # Skapa innehåll
638
+ subject = f"Support förfrågan - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
639
+
640
+ content = f"""
641
+ *Användarinformation*
642
+ - *Områdeskod:* {områdeskod or 'Ej angiven'}
643
+ - *Uttagsnummer:* {uttagsnummer or 'Ej angiven'}
644
+ - *Email:* {email}
645
+ - *Tidpunkt:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
646
+
647
+ *Chatthistorik:*
648
+ {chat_content}
649
+ """
650
+
651
+ # Skicka till Slack
652
+ return send_to_slack(subject, content, "#e76f51")
653
+ except Exception as e:
654
+ print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
655
+ return False
656
+
657
+ # --- Schemaläggning av rapporter ---
658
  def run_scheduler():
659
+ """Kör schemaläggaren i en separat tråd med förenklad statusrapportering."""
660
+ # Flagga för att kontrollera om tråden ska fortsätta köra
661
+ running = True
662
+
663
+ try:
664
+ # Använd den förenklade funktionen för rapportering
665
+ schedule.every().day.at("08:00").do(simple_status_report)
666
+ schedule.every().day.at("12:00").do(simple_status_report)
667
+ schedule.every().day.at("17:00").do(simple_status_report)
668
+
669
+ # Veckorapport måndagar
670
+ schedule.every().monday.at("09:00").do(lambda: send_to_slack(
671
+ "Veckostatistik",
672
+ f"*ChargeNode AI Bot - Veckostatistik*\n\n{json.dumps(generate_monthly_stats(7), indent=2)}",
673
+ "#3498db"
674
+ ))
675
+
676
+ while running:
677
+ try:
678
+ schedule.run_pending()
679
+ time.sleep(60) # Kontrollera varje minut
680
+ except Exception as e:
681
+ print(f"Fel i schemaläggaren: {e}")
682
+ time.sleep(300) # Vänta 5 minuter vid fel
683
+ except Exception as e:
684
+ print(f"Kritiskt fel i schemaläggaren: {e}")
685
+ finally:
686
+ print("Schemaläggaren avslutad")
687
+
688
+ # Starta schemaläggaren i en separat tråd
689
+ scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
690
+ scheduler_thread.start()
691
+
692
+ # Registrera en atexit-funktion för att städa upp vid avslut
693
+
694
  def cleanup():
695
+ """Städa upp resurser vid avslut."""
696
  print("Städar upp resurser...")
697
+ # Stäng scheduler om möjligt
698
+ schedule.clear()
699
+
700
+ # Stäng CommitScheduler om den är aktiv
701
+ if 'scheduler' in globals() and scheduler:
 
702
  try:
703
+ scheduler.stop()
704
+ print("CommitScheduler stoppad")
705
+ except Exception as e:
706
+ print(f"Kunde inte stoppa CommitScheduler: {e}")
707
+
708
+ # Stäng eventuella öppna filer
709
  try:
710
+ # Försök att stänga alla öppna filer
711
  import gc
712
  for obj in gc.get_objects():
713
+ if isinstance(obj, io.IOBase) and not obj.closed:
714
+ try:
715
+ obj.close()
716
+ except:
717
+ pass
718
+ except Exception as e:
719
+ print(f"Fel vid stängning av filer: {e}")
720
+
721
  atexit.register(cleanup)
722
 
723
+ # Kör en statusrapport vid uppstart för att verifiera att allt fungerar
724
+ try:
725
+ print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
726
+ # Anropa inte direkt här - sker i schemaläggaren
727
+ except Exception as e:
728
+ print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
729
 
730
+ # Definiera respond och chat-relaterade funktioner före Gradio UI
731
  def respond(message, chat_history, request: gr.Request):
732
  global last_log, last_log_lock
733
+
734
+ # Validera indata
735
+ if not message or not isinstance(message, str):
736
+ print(f"Varning: Ogiltigt meddelande: {type(message)}")
737
+ message = str(message) if message is not None else ""
738
+
739
+ # Begränsa meddelandets längd för att förhindra överbelastning
740
+ if len(message) > 1000:
741
+ message = message[:1000] + "..."
742
+ print("Meddelande trunkerat på grund av längd")
743
+
744
+ start = time.time()
745
+ try:
746
+ response = generate_answer(message)
747
+ except Exception as e:
748
+ print(f"Fel vid generering av svar: {e}")
749
+ response = f"Tyvärr uppstod ett tekniskt fel. Vänligen försök igen eller kontakta support@chargenode.eu. Felkod: {str(e)[:50]}"
750
+
751
+ elapsed = round(time.time() - start, 2)
752
+
753
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
754
  session_id = str(uuid.uuid4())
755
+
756
+ # Använd session_id från tidigare logg om det finns
757
  with last_log_lock:
758
+ if last_log and 'session_id' in last_log:
759
+ session_id = last_log.get('session_id')
760
+
761
+ # Säker hantering av request-objektet
762
+ user_id = "okänd"
763
+ ua_str = ""
764
+ ref = ""
765
+ ip = ""
766
+
767
  if request:
768
  try:
769
+ user_id = request.client.host if hasattr(request.client, 'host') else "okänd"
770
+ ua_str = request.headers.get("user-agent", "") if hasattr(request, 'headers') else ""
771
+ ref = request.headers.get("referer", "") if hasattr(request, 'headers') else ""
772
+ ip = request.headers.get("x-forwarded-for", user_id).split(",")[0] if hasattr(request, 'headers') else user_id
773
+ except Exception as e:
774
+ print(f"Fel vid läsning av request-data: {e}")
775
+
776
+ # Säker parsing av user agent
777
+ try:
778
+ ua = parse_ua(ua_str)
779
+ browser = f"{ua.browser.family} {ua.browser.version_string}"
780
+ osys = f"{ua.os.family} {ua.os.version_string}"
781
+ except Exception as e:
782
+ print(f"Fel vid parsing av user agent: {e}")
783
+ browser = "okänd"
784
+ osys = "okänd"
785
+
786
+ # Bestäm plattform
787
+ platform = "webb"
788
+ try:
789
+ if ref:
790
+ if "chargenode.eu" in ref:
791
+ platform = "chargenode.eu"
792
+ elif "localhost" in ref:
793
+ platform = "test"
794
+ elif "app" in ref:
795
+ platform = "app"
796
+ except Exception as e:
797
+ print(f"Fel vid bestämning av plattform: {e}")
798
+
799
+ # Skapa loggdata
800
+ log_data = {
801
+ "timestamp": timestamp,
802
+ "user_id": user_id,
803
+ "session_id": session_id,
804
+ "user_message": message,
805
+ "bot_reply": response,
806
+ "response_time": elapsed,
807
+ "ip": ip,
808
+ "browser": browser,
809
+ "os": osys,
810
+ "platform": platform
811
+ }
812
+
813
+ # Använd den förbättrade loggfunktionen
814
  safe_append_to_log(log_data)
815
+
816
+ # Uppdatera last_log säkert
817
+ with last_log_lock:
818
+ last_log = log_data.copy() # Använd en kopia för att undvika race conditions
819
 
820
+ # Skicka varje konversation direkt till Slack
821
  try:
822
+ # Konversationsinnehåll
823
+ conversation_content = f"""
824
+ *Ny konversation {timestamp}*
825
+
826
+ *Användare:* {message}
827
+
828
+ *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
829
 
830
+ *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform}
831
+ """
832
+ # Skicka asynkront för att inte blockera svarstiden
833
+ threading.Thread(
834
+ target=lambda: send_to_slack(f"Ny konversation", conversation_content),
835
+ daemon=True
836
+ ).start()
837
+ except Exception as e:
838
+ print(f"Kunde inte skicka konversation till Slack: {e}")
839
+
840
+ # Uppdatera chatthistorik
841
+ try:
842
+ chat_history.append({"role": "user", "content": message})
843
+ chat_history.append({"role": "assistant", "content": response})
844
+ except Exception as e:
845
+ print(f"Fel vid uppdatering av chatthistorik: {e}")
846
+ # Försök återställa chatthistoriken om något går fel
847
+ if not chat_history:
848
+ chat_history = []
849
+ chat_history.append({"role": "user", "content": message})
850
+ chat_history.append({"role": "assistant", "content": response})
851
+
852
+ # Skapa JavaScript för att scrolla till senaste botmeddelandet
853
+ scroll_js = """
854
+ setTimeout(function() {
855
+ console.log("Executing scroll script");
856
+ const messages = document.querySelectorAll('.message');
857
+ if (messages.length > 0) {
858
+ console.log("Found messages:", messages.length);
859
+ const lastMessage = messages[messages.length - 1];
860
+ console.log("Scrolling to last message");
861
+ lastMessage.scrollIntoView({ behavior: 'smooth', block: 'start' });
862
+ } else {
863
+ console.log("No messages found, trying alternative selectors");
864
+ // Alternativa selektorer för Hugging Face
865
+ const altMessages = document.querySelectorAll('.bot, [data-testid="bot"], .message-wrap > div:last-child');
866
+ if (altMessages.length > 0) {
867
+ console.log("Found messages with alt selector:", altMessages.length);
868
+ const lastAltMessage = altMessages[altMessages.length - 1];
869
+ console.log("Scrolling to last alt message");
870
+ lastAltMessage.scrollIntoView({ behavior: 'smooth', block: 'start' });
871
+ } else {
872
+ console.log("No messages found with any selector");
873
+ }
874
+ }
875
+ }, 100); // Vänta 100 millisekunder innan scroll
876
+
877
+ // Försök igen efter lite längre tid om första försöket misslyckas
878
+ setTimeout(function() {
879
+ console.log("Executing delayed scroll script");
880
+ const messages = document.querySelectorAll('.message, .bot, [data-testid="bot"], .message-wrap > div:last-child');
881
+ if (messages.length > 0) {
882
+ console.log("Found messages in delayed script:", messages.length);
883
+ const lastMessage = messages[messages.length - 1];
884
+ console.log("Scrolling to last message in delayed script");
885
+ lastMessage.scrollIntoView({ behavior: 'smooth', block: 'start' });
886
+ }
887
+ }, 500); // Vänta 500 millisekunder innan andra försöket
888
+ """
889
+
890
+ return "", chat_history, gr.update(javascript=scroll_js)
891
 
892
  def format_chat_preview(chat_history):
893
+ if not chat_history:
894
+ return "Ingen chatthistorik att visa."
895
+
896
+ preview = ""
897
+ for msg in chat_history:
898
+ sender = "Användare" if msg["role"] == "user" else "Bot"
899
+ content = msg["content"]
900
+ if len(content) > 100: # Truncate long messages
901
+ content = content[:100] + "..."
902
+ preview += f"**{sender}:** {content}\n\n"
903
+
904
+ return preview
905
 
906
  def show_support_form(chat_history):
907
+ preview = format_chat_preview(chat_history)
908
+ return {
909
+ chat_interface: gr.Group(visible=False),
910
+ support_interface: gr.Group(visible=True),
911
+ success_interface: gr.Group(visible=False),
912
+ chat_preview: preview
913
+ }
914
 
915
  def back_to_chat():
916
+ return {
917
+ chat_interface: gr.Group(visible=True),
918
+ support_interface: gr.Group(visible=False),
919
+ success_interface: gr.Group(visible=False)
920
+ }
921
 
922
  def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
923
+ """Hanterar formulärinskickningen med bättre felhantering."""
924
+ print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
925
+
926
+ # Validera input med tydligare loggning
927
+ validation_errors = []
928
+
929
+ if områdeskod and not områdeskod.isdigit():
930
+ print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
931
+ validation_errors.append("Områdeskod måste vara numerisk.")
932
+ else:
933
+ print(f"Validerar områdeskod: '{områdeskod}' (ok)")
934
+
935
+ if uttagsnummer and not uttagsnummer.isdigit():
936
+ print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
937
+ validation_errors.append("Uttagsnummer måste vara numerisk.")
938
+ else:
939
+ print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
940
+
941
+ if not email:
942
+ print("Validerar email: (saknas)")
943
+ validation_errors.append("En giltig e-postadress krävs.")
944
+ elif '@' not in email or '.' not in email.split('@')[1]:
945
+ print(f"Validerar email: '{email}' (felaktigt format)")
946
+ validation_errors.append("En giltig e-postadress krävs.")
947
+ else:
948
+ print(f"Validerar email: '{email}' (ok)")
949
+
950
+ # Om det finns valideringsfel
951
+ if validation_errors:
952
+ print(f"Valideringsfel: {validation_errors}")
953
+ return {
954
+ chat_interface: gr.Group(visible=False),
955
+ support_interface: gr.Group(visible=True),
956
+ success_interface: gr.Group(visible=False),
957
+ chat_preview: "\n".join(["**Fel:**"] + validation_errors)
958
+ }
959
+
960
+ # Om formuläret klarade valideringen, försök skicka till Slack
961
  try:
962
+ print("Försöker skicka supportförfrågan till Slack...")
963
+
964
+ # Skapa en förenklad chathistorik för loggning
965
+ chat_summary = []
966
+ for msg in chat_history:
967
+ if 'role' in msg and 'content' in msg:
968
+ chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
969
+ print(f"Chatthistorik att skicka: {chat_summary}")
970
+
971
+ # Skicka till Slack
972
  success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
973
+
974
+ if success:
975
+ print("Support-förfrågan skickad till Slack framgångsrikt")
976
+ return {
977
+ chat_interface: gr.Group(visible=False),
978
+ support_interface: gr.Group(visible=False),
979
+ success_interface: gr.Group(visible=True)
980
+ }
981
+ else:
982
+ print("Support-förfrågan till Slack misslyckades")
983
+ return {
984
+ chat_interface: gr.Group(visible=False),
985
+ support_interface: gr.Group(visible=True),
986
+ success_interface: gr.Group(visible=False),
987
+ chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
988
+ }
989
+ except Exception as e:
990
+ print(f"Oväntat fel vid hantering av support-formulär: {e}")
991
+ return {
992
+ chat_interface: gr.Group(visible=False),
993
+ support_interface: gr.Group(visible=True),
994
+ success_interface: gr.Group(visible=False),
995
+ chat_preview: f"**Ett fel uppstod: {str(e)}**"
996
+ }
997
 
998
+ # --- Gradio UI ---
999
+ initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
1000
 
1001
  custom_css = """
1002
+ body {background-color: #f7f7f7; font-family: Arial, sans-serif; margin: 0; padding: 0;}
1003
+ h1 {font-family: Helvetica, sans-serif; color: #2a9d8f; text-align: center; margin-bottom: 0.5em;}
1004
+ .gradio-container {max-width: 400px; margin: 0; padding: 10px; position: fixed; bottom: 20px; right: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); border-radius: 10px; background-color: #fff;}
1005
+ #chatbot_conversation { max-height: 300px; overflow-y: auto; }
1006
+ .message-wrap { scroll-behavior: smooth; }
1007
+ .message.bot:last-child { scroll-margin-top: 100px; }
1008
+ .support-form-container { margin-top: 40px; }
1009
+ .support-form-container .gr-form { margin-top: 15px; }
1010
+ .gr-button {background-color: #2a9d8f; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; margin: 5px;}
1011
+ .gr-button:hover {background-color: #264653;}
1012
+ .support-btn {background-color: #000000; color: #ffffff; margin-top: 5px; margin-bottom: 10px;}
1013
+ .support-btn:hover {background-color: #333333;}
1014
+ .flex-row {display: flex; flex-direction: row; gap: 5px;}
1015
+ .gr-form {padding: 10px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 10px;}
1016
+ .chat-preview {max-height: 150px; overflow-y: auto; border: 1px solid #eee; padding: 8px; margin-top: 10px; font-size: 12px; background-color: #f9f9f9;}
1017
+ .success-message {font-size: 16px; font-weight: normal; margin-bottom: 15px;}
1018
+ /* Dölj Gradio-footer */
1019
+ footer {display: none !important;}
1020
+ .footer {display: none !important;}
1021
+ .gr-footer {display: none !important;}
1022
+ .gradio-footer {display: none !important;}
1023
+ .gradio-container .footer {display: none !important;}
1024
+ .gradio-container .gr-footer {display: none !important;}
1025
  """
1026
 
1027
+ # VIKTIGT: Alla komponenter och eventkopplingar definieras inuti Blocks-kontexten
1028
  with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
1029
+ gr.Markdown("Ställ din fråga om ChargeNodes produkter och tjänster nedan. Om du inte gillar botten, så ring oss gärna på 010 – 205 10 55")
1030
+
1031
+ # Chat interface
1032
  with gr.Group(visible=True) as chat_interface:
1033
+ chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
1034
+ chatbot.like(vote, None, None)
1035
+
 
 
 
 
 
 
 
 
 
 
1036
  with gr.Row():
1037
+ msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
1038
+
1039
+ with gr.Row():
1040
+ with gr.Column(scale=1):
1041
+ clear = gr.Button("Rensa")
1042
+ with gr.Column(scale=1):
1043
+ support_btn = gr.Button("Behöver du mer hjälp?", elem_classes="support-btn")
1044
+
1045
+ # Lägg till anpassad JavaScript för att styra scrollning och förhindra auto-scroll
 
 
 
 
 
 
 
 
 
 
 
 
1046
  js_code = """
1047
+ // Förhindra Gradio's automatiska scrollning till botten
1048
+ (function() {
1049
+ // Spara originella scrollTo och scrollIntoView funktioner
1050
+ const originalScrollTo = window.scrollTo;
1051
+ const originalElementScrollIntoView = Element.prototype.scrollIntoView;
1052
+ const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
1053
+
1054
+ // Övervaka och förhindra automatisk scrollning till botten
1055
+ window.scrollTo = function(x, y) {
1056
+ console.log('scrollTo intercepted:', x, y);
1057
+ // Tillåt bara scrollning som inte är till botten av sidan
1058
+ if (typeof y !== 'number' || y < document.body.scrollHeight - window.innerHeight - 100) {
1059
+ originalScrollTo.apply(this, arguments);
1060
+ } else {
1061
+ console.log('Prevented automatic scroll to bottom');
1062
+ }
1063
+ };
1064
+
1065
+ // Övervaka scrollIntoView anrop
1066
+ Element.prototype.scrollIntoView = function(options) {
1067
+ console.log('Element scrollIntoView intercepted', this, options);
1068
+ // Kontrollera om elementet är ett botmeddelande eller en del av chattcontainern
1069
+ if (this.classList && (
1070
+ this.classList.contains('message') ||
1071
+ this.classList.contains('bot') ||
1072
+ this.closest('.message-wrap')
1073
+ )) {
1074
+ console.log('Handling chat element scrollIntoView');
1075
+ // Anropa vår egen scrollningsfunktion istället
1076
+ handleChatScroll();
1077
+ return;
1078
  }
1079
+ // Annars, låt originella funktionen köra
1080
+ originalElementScrollIntoView.apply(this, arguments);
1081
+ };
1082
+
1083
+ HTMLElement.prototype.scrollIntoView = Element.prototype.scrollIntoView;
1084
+
1085
+ console.log('Scroll interception set up');
1086
+ })();
1087
+
1088
+ // Huvudfunktion för att hantera chattscrollning
1089
+ function handleChatScroll() {
1090
+ console.log('handleChatScroll called');
1091
+
1092
+ // Hitta chattcontainern
1093
+ const chatContainers = [
1094
+ document.querySelector('#chatbot_conversation .message-wrap'),
1095
+ document.querySelector('.gradio-container .chat'),
1096
+ document.querySelector('.gradio-container [id^="component-"] .chat'),
1097
+ document.querySelector('.gradio-container .message-wrap')
1098
+ ].filter(Boolean);
1099
+
1100
+ if (chatContainers.length === 0) {
1101
+ console.log('No chat container found');
1102
+ return;
1103
+ }
1104
+
1105
+ const chatContainer = chatContainers[0];
1106
+ console.log('Found chat container:', chatContainer);
1107
+
1108
+ // Hitta alla botmeddelanden
1109
+ const botMessages = [
1110
+ ...Array.from(document.querySelectorAll('.message.bot')),
1111
+ ...Array.from(document.querySelectorAll('[data-testid="bot"]')),
1112
+ ...Array.from(document.querySelectorAll('.message:not(.user)'))
1113
+ ].filter(Boolean);
1114
+
1115
+ if (botMessages.length === 0) {
1116
+ console.log('No bot messages found');
1117
+ return;
1118
+ }
1119
+
1120
+ // Hitta det senaste botmeddelandet
1121
+ const latestBotMessage = botMessages[botMessages.length - 1];
1122
+ console.log('Latest bot message:', latestBotMessage);
1123
+
1124
+ // Beräkna position för att visa botmeddelandet i toppen
1125
+ try {
1126
+ const containerRect = chatContainer.getBoundingClientRect();
1127
+ const messageRect = latestBotMessage.getBoundingClientRect();
1128
+ const relativeTop = messageRect.top - containerRect.top;
1129
+
1130
+ // Scrolla så att botmeddelandet är i toppen
1131
+ chatContainer.scrollTop = chatContainer.scrollTop + relativeTop - 10; // 10px marginal
1132
+ console.log('Scrolled chat container to show bot message at top');
1133
+
1134
+ // Förhindra ytterligare scrollning under en kort period
1135
+ chatContainer.style.overflowY = 'hidden';
1136
+ setTimeout(() => {
1137
+ chatContainer.style.overflowY = '';
1138
+ }, 500);
1139
+ } catch (e) {
1140
+ console.error('Error during manual scrolling:', e);
1141
  }
1142
  }
1143
+
1144
+ // Lyssna DOM-ändringar för att upptäcka nya meddelanden
1145
+ document.addEventListener('DOMContentLoaded', function() {
1146
+ console.log('DOM loaded, setting up observers');
1147
+
1148
+ // Använd MutationObserver för att upptäcka nya meddelanden
1149
+ const observer = new MutationObserver(function(mutations) {
1150
+ let newBotMessage = false;
1151
+
1152
+ mutations.forEach(function(mutation) {
1153
+ if (mutation.type === 'childList' && mutation.addedNodes.length) {
1154
+ mutation.addedNodes.forEach(function(node) {
1155
+ if (node.nodeType === 1) { // Element node
1156
+ // Kontrollera om det är ett botmeddelande
1157
+ if (node.classList && (
1158
+ node.classList.contains('bot') ||
1159
+ node.classList.contains('message')
1160
+ )) {
1161
+ newBotMessage = true;
1162
+ }
1163
+ // Eller om det innehåller ett botmeddelande
1164
+ else if (node.querySelector && (
1165
+ node.querySelector('.bot') ||
1166
+ node.querySelector('.message:not(.user)')
1167
+ )) {
1168
+ newBotMessage = true;
1169
+ }
1170
+ }
1171
  });
1172
  }
1173
+ });
1174
+
1175
+ if (newBotMessage) {
1176
+ console.log('New bot message detected');
1177
+ // Använd flera timeouts för att säkerställa att scrollningen fungerar
1178
+ // efter att alla DOM-uppdateringar är klara
1179
+ setTimeout(handleChatScroll, 100);
1180
+ setTimeout(handleChatScroll, 300);
1181
+ setTimeout(handleChatScroll, 500);
1182
+ setTimeout(handleChatScroll, 1000);
1183
  }
 
1184
  });
1185
+
1186
+ // Starta övervakning av hela dokumentet
1187
+ observer.observe(document.body, {
1188
+ childList: true,
1189
+ subtree: true
1190
+ });
1191
+
1192
+ console.log('MutationObserver started');
1193
+
1194
+ // Lyssna även på klickhändelser för att hantera scrollning efter användarinteraktion
1195
+ document.addEventListener('click', function(event) {
1196
+ // Fördröj för att låta eventuella UI-uppdateringar slutföras
1197
+ setTimeout(handleChatScroll, 300);
1198
+ });
1199
+ });
1200
+
1201
+ // Lägg till CSS för att förbättra scrollningsbeteendet
1202
+ const style = document.createElement('style');
1203
+ style.textContent = `
1204
+ #chatbot_conversation .message-wrap {
1205
+ scroll-behavior: smooth !important;
1206
+ overflow-anchor: none !important; /* Förhindra automatisk ankring */
1207
+ }
1208
+ .message.bot {
1209
+ scroll-margin-top: 10px !important;
1210
+ }
1211
+ `;
1212
+ document.head.appendChild(style);
1213
  """
1214
  app.load(js=js_code)
1215
+
1216
+ # Support form interface (initially hidden)
1217
+ with gr.Group(visible=False, elem_classes="support-form-container") as support_interface:
1218
+ gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
1219
+
1220
+ with gr.Group(elem_classes="gr-form"):
1221
+ områdeskod = gr.Textbox(label="Områdeskod", placeholder="Områdeskod (valfritt)", info="Numeriskt värde")
1222
+ uttagsnummer = gr.Textbox(label="Uttagsnummer", placeholder="Uttagsnummer (valfritt)", info="Numeriskt värde")
1223
+ email = gr.Textbox(label="Din email adress", placeholder="din@email.se", info="Email adress krävs")
1224
+
1225
+ gr.Markdown("### Chat som skickas till support:")
1226
+ chat_preview = gr.Markdown(elem_classes="chat-preview")
1227
+
1228
+ with gr.Row():
1229
+ back_btn = gr.Button("Tillbaka")
1230
+ send_support_btn = gr.Button("Skicka")
1231
+
1232
+ # Success message (initially hidden)
1233
+ with gr.Group(visible=False) as success_interface:
1234
+ gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
1235
+ back_to_chat_btn = gr.Button("Tillbaka till chatten")
1236
+
1237
+ # VIKTIGT: Händelsehanterare definieras INOM gr.Blocks-kontexten, efter att komponenterna definierats
1238
+ msg.submit(respond, [msg, chatbot], [msg, chatbot])
1239
+ clear.click(lambda: None, None, chatbot, queue=False)
1240
+ support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
1241
+ back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
1242
+ back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
1243
+ send_support_btn.click(
1244
+ submit_support_form,
1245
+ [områdeskod, uttagsnummer, email, chatbot],
1246
+ [chat_interface, support_interface, success_interface, chat_preview]
1247
+ )
1248
 
 
 
 
 
 
 
 
 
 
 
1249
  if __name__ == "__main__":
1250
+ app.launch(share=True)