galbendavids commited on
Commit
e405ff6
·
1 Parent(s): 395afb7

תיקונים: היסטוריה, תשובות מפורטות, ניגודיות כפתורים, תיקון CreationDate

Browse files
.gitignore CHANGED
@@ -16,3 +16,4 @@ uvicorn.log
16
  .uvicorn.log
17
  .query_history.json
18
  req
 
 
16
  .uvicorn.log
17
  .query_history.json
18
  req
19
+ feedback_transformed.csv
README.md CHANGED
@@ -31,6 +31,24 @@ license: mit
31
  - Python 3.10+
32
  - קובץ `Feedback.csv` עם העמודות: ID, ServiceName, Level, Text, CreationDate (אופציונלי)
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  ### התקנה
35
 
36
  ```bash
@@ -87,7 +105,8 @@ python run.py
87
  │ └── static/
88
  │ ├── index.html # ממשק משתמש
89
  │ └── app.js # לוגיקת frontend
90
- ├── Feedback.csv # נתוני המשובים (לא ב-git)
 
91
  ├── .env # API keys (לא ב-git)
92
  ├── requirements.txt # תלויות Python
93
  ├── run.py # נקודת כניסה
 
31
  - Python 3.10+
32
  - קובץ `Feedback.csv` עם העמודות: ID, ServiceName, Level, Text, CreationDate (אופציונלי)
33
 
34
+ ### תיקון עמודת CreationDate
35
+
36
+ אם קובץ `Feedback.csv` מכיל עמודת `CreationDate` בפורמט לא תקין (למשל `MM:SS.s` - דקות:שניות.חלקי שנייה), יש להריץ את הסקריפט לתיקון:
37
+
38
+ ```bash
39
+ python scripts/fix_creation_date.py
40
+ ```
41
+
42
+ הסקריפט:
43
+ 1. מנתח את פורמט `MM:SS.s` וממיר אותו לשניות
44
+ 2. מפיץ את הרשומות על פני תקופה של שנה (מ-2020-01-01)
45
+ 3. משתמש בערך `MM:SS.s` כזמן ביום (שעות:דקות:שניות)
46
+ 4. שומר את הקובץ המתוקן כ-`feedback_transformed.csv`
47
+
48
+ המערכת משתמשת ב-`feedback_transformed.csv` כברירת מחדל (אם קיים), אחרת ב-`Feedback.csv`.
49
+
50
+ **הערה**: אם יש לך תאריכים אמיתיים, עדכן את `CSV_PATH` ב-`.env` או העלה את הקובץ עם התאריכים הנכונים.
51
+
52
  ### התקנה
53
 
54
  ```bash
 
105
  │ └── static/
106
  │ ├── index.html # ממשק משתמש
107
  │ └── app.js # לוגיקת frontend
108
+ ├── Feedback.csv # נתוני המשובים המקוריים (לא ב-git)
109
+ ├── feedback_transformed.csv # נתוני המשובים עם CreationDate מתוקן (לא ב-git)
110
  ├── .env # API keys (לא ב-git)
111
  ├── requirements.txt # תלויות Python
112
  ├── run.py # נקודת כניסה
app/api.py CHANGED
@@ -46,15 +46,16 @@ def save_history() -> None:
46
  Save query history to disk.
47
 
48
  This is a best-effort operation - if saving fails (e.g., disk full,
49
- permissions issue), the error is silently ignored to avoid breaking
50
- the main application flow. History is stored in `.query_history.json`.
51
  """
52
  try:
53
  with history_file.open("w", encoding="utf-8") as f:
54
  json.dump(history, f, ensure_ascii=False, indent=2)
55
- except Exception:
56
- # Best-effort persistence; ignore errors to avoid breaking main flow
57
- pass
 
58
 
59
 
60
  class QueryRequest(BaseModel):
@@ -168,12 +169,22 @@ def query_sql(req: QueryRequest) -> SQLQueryResponse:
168
  "row_count": len(qr.result) if not qr.error else 0
169
  })
170
 
171
- # Save to history
172
  try:
173
- history.append({"query": result.user_query, "response": {"summary": result.summary}})
 
 
 
 
174
  save_history()
175
- except Exception:
176
- pass
 
 
 
 
 
 
177
 
178
  return SQLQueryResponse(
179
  query=result.user_query,
 
46
  Save query history to disk.
47
 
48
  This is a best-effort operation - if saving fails (e.g., disk full,
49
+ permissions issue), the error is logged but doesn't break the main flow.
50
+ History is stored in `.query_history.json`.
51
  """
52
  try:
53
  with history_file.open("w", encoding="utf-8") as f:
54
  json.dump(history, f, ensure_ascii=False, indent=2)
55
+ print(f"History saved successfully to {history_file}", flush=True)
56
+ except Exception as e:
57
+ # Log error but don't break main flow
58
+ print(f"Warning: Could not save history to {history_file}: {e}", flush=True)
59
 
60
 
61
  class QueryRequest(BaseModel):
 
169
  "row_count": len(qr.result) if not qr.error else 0
170
  })
171
 
172
+ # Save to history - ensure it's always saved
173
  try:
174
+ history.append({
175
+ "query": result.user_query,
176
+ "response": {"summary": result.summary},
177
+ "timestamp": __import__("datetime").datetime.now().isoformat()
178
+ })
179
  save_history()
180
+ print(f"History saved: {len(history)} entries", flush=True)
181
+ except Exception as e:
182
+ print(f"Error saving history: {e}", flush=True)
183
+ # Try to save anyway, even if there's an error
184
+ try:
185
+ save_history()
186
+ except:
187
+ pass
188
 
189
  return SQLQueryResponse(
190
  query=result.user_query,
app/config.py CHANGED
@@ -29,7 +29,8 @@ class Settings:
29
  gemini_api_key: str | None = os.getenv("GEMINI_API_KEY")
30
 
31
  # CSV data file path - relative to project root
32
- csv_path: str = os.getenv("CSV_PATH", "Feedback.csv")
 
33
 
34
  # Column names in the CSV file - adjust if your CSV uses different column names
35
  text_column: str = os.getenv("TEXT_COLUMN", "Text")
 
29
  gemini_api_key: str | None = os.getenv("GEMINI_API_KEY")
30
 
31
  # CSV data file path - relative to project root
32
+ # Default to feedback_transformed.csv which has properly formatted CreationDate
33
+ csv_path: str = os.getenv("CSV_PATH", "feedback_transformed.csv")
34
 
35
  # Column names in the CSV file - adjust if your CSV uses different column names
36
  text_column: str = os.getenv("TEXT_COLUMN", "Text")
app/sql_service.py CHANGED
@@ -483,7 +483,7 @@ class SQLFeedbackService:
483
  results_text += qr.result.to_string(index=False)
484
  results_text += "\n\n"
485
 
486
- prompt = f"""אתה אנליסט עסקי בכיר במשרד הפנים, מומחה בייעול תהליכים דיגיטליים ושיפור חוויות המשתמשים.
487
 
488
  המשתמש שאל שאלה על משובי משתמשים על שירותים דיגיטליים.
489
 
@@ -495,29 +495,38 @@ class SQLFeedbackService:
495
 
496
  המשימה שלך: כתוב תשובה מסכמת, ברורה ובשפה חופשית שמבוססת על התוצאות.
497
 
498
- חשוב מאוד - קריטי:
 
 
 
499
  - אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
500
  - אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
501
  - אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
502
  - תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
503
 
504
- דרישות:
505
- 1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים)
506
  2. תשובה ברורה ומסודרת - לא גיבוב של מילים
507
- 3. כלול מספרים מדויקים מהתוצאות
508
- 4. הסבר את המשמעות העסקית של הממצאים
509
- 5. כלול המלצות מעשיות לשיפור
510
- 6. כתוב בעברית מקצועית וקולחת
511
- 7. תן תשובה שמראה הבנה עמוקה של הנתונים
512
-
513
- מבנה התשובה:
514
- 1. פתיחה - סיכום מנהלים קצר (2-3 משפטים)
 
515
  2. ניתוח מפורט של הממצאים (3-4 פסקאות)
516
- - אם נדרש סיווג לפי שירותים/דירוגים/תאריכים - כלול כאן ניתוח נפרד לכל קטגוריה
 
517
  3. תובנות עסקיות והמלצות (2-3 פסקאות)
518
- 4. סיכום (1-2 משפטים)
 
 
519
 
520
- אם יש שגיאות בשאילתות, ציין זאת בתשובה, אבל עדיין תן תשובה מפורטת על בסיס התוצאות שהתקבלו."""
 
 
521
 
522
  # Try Gemini first
523
  if settings.gemini_api_key and genai is not None:
@@ -555,19 +564,24 @@ class SQLFeedbackService:
555
  כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
556
  התשובה חייבת לענות ישירות על השא��ה: {query}
557
 
558
- חשוב מאוד - קריטי:
 
 
 
559
  - אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
560
  - אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
561
  - אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
562
  - תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
563
 
564
- דרישות:
565
- 1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים)
566
  2. תשובה שמתמקדת ישירות בשאלה שנשאלה
567
- 3. כלול מספרים מדויקים מהתוצאות
568
- 4. הסבר את המשמעות העסקית של הממצאים
569
- 5. כלול המלצות מעשיות לשיפור
570
- 6. כתוב בעברית מקצועית וקולחת"""
 
 
571
 
572
  try:
573
  response = model.generate_content(improvement_prompt, generation_config=generation_config)
@@ -619,11 +633,24 @@ class SQLFeedbackService:
619
  כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
620
  התשובה חייבת לענות ישירות על השאלה: {query}
621
 
622
- חשוב מאוד - קריטי:
 
 
 
623
  - אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
624
  - אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
625
  - אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
626
- - תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!"""
 
 
 
 
 
 
 
 
 
 
627
 
628
  try:
629
  response = client.chat.completions.create(
@@ -650,20 +677,36 @@ class SQLFeedbackService:
650
  # This ensures we always return a meaningful answer, not just a status message
651
  successful_results = [r for r in query_results if not r.error and len(r.result) > 0]
652
  if successful_results:
653
- fallback_text = f"בוצעו {len(sql_queries)} שאילתות, {len(successful_results)} הצליחו.\n\n"
654
- fallback_text += "סיכום התוצאות:\n\n"
 
 
655
  for i, qr in enumerate(successful_results, 1):
656
- fallback_text += f"שאילתה {i}: {qr.query}\n"
657
- fallback_text += f"מספר שורות: {len(qr.result)}\n"
658
- # Include first few rows as summary
 
 
659
  if len(qr.result) > 0:
660
- fallback_text += "תוצאות (דוגמאות):\n"
 
 
 
 
 
 
 
 
 
 
661
  fallback_text += qr.result.head(5).to_string(index=False)
662
  fallback_text += "\n\n"
 
 
663
  return fallback_text
664
  else:
665
  # If no successful results, still provide a helpful message
666
- return f"בוצעו {len(sql_queries)} שאילתות, אך לא התקבלו תוצאות. ייתכן שהנתונים לא מכילים מידע התואם לשאלה."
667
 
668
  def _generate_visualizations(self, query_results: List[SQLQueryResult]) -> Optional[List[Dict[str, Any]]]:
669
  """
 
483
  results_text += qr.result.to_string(index=False)
484
  results_text += "\n\n"
485
 
486
+ prompt = f"""אתה אנליסט עסקי בכיר במשרד הפנים, מומחה בייעול תהליכים דיגיטליים ושיפור חוויות המשתמשים בעולם התוכן הממשלתי.
487
 
488
  המשתמש שאל שאלה על משובי משתמשים על שירותים דיגיטליים.
489
 
 
495
 
496
  המשימה שלך: כתוב תשובה מסכמת, ברורה ובשפה חופשית שמבוססת על התוצאות.
497
 
498
+ ⚠️ חובה קריטית - תשובה מילולית מפורטת:
499
+ - אתה חייב לכתוב תשובה מילולית מפורטת ומשמעותית - לא רק לרשום את השאילתות!
500
+ - התשובה חייבת להיות כתובה בשפה טבעית, מקצועית וקולחת
501
+ - התשובה חייבת להסביר את הממצאים, לא רק להציג אותם
502
  - אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
503
  - אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
504
  - אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
505
  - תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
506
 
507
+ דרישות מקצועיות:
508
+ 1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים) - חובה!
509
  2. תשובה ברורה ומסודרת - לא גיבוב של מילים
510
+ 3. כלול מספרים מדויקים מהתוצאות עם הסבר על המשמעות שלהם
511
+ 4. הסבר את המשמעות העסקית של הממצאים בהקשר של תהליכים דיגיטליים ממשלתיים
512
+ 5. כלול המלצות מעשיות לשיפור תהליכים ושירותים דיגיטליים
513
+ 6. כתוב בעברית מקצועית וקולחת, תוך שימוש במונחים מקצועיים מתחום הממשל הדיגיטלי
514
+ 7. תן תשובה שמראה הבנה עמוקה של הנתונים והקשר שלהם לשיפור חוויות המשתמשים
515
+ 8. התשובה חייבת להיות רלוונטית לשאלה שנשאלה - לא תשובה כללית
516
+
517
+ מבנה התשובה (חובה):
518
+ 1. פתיחה - סיכום מנהלים קצר (2-3 משפטים) שמסכם את הממצאים העיקריים
519
  2. ניתוח מפורט של הממצאים (3-4 פסקאות)
520
+ - אם נדרש סיווג לפי שירותים/דירוגים/תאריכים - כלול כאן ניתוח נפרד ומפורט לכל קטגוריה
521
+ - הסבר את המספרים והנתונים בהקשר של שירותים דיגיטליים ממשלתיים
522
  3. תובנות עסקיות והמלצות (2-3 פסקאות)
523
+ - תובנות על תהליכים דיגיטליים שניתן לשפר
524
+ - המלצות מעשיות לשיפור חוויות המשתמשים
525
+ 4. סיכום (1-2 משפטים) - מסקנות עיקריות
526
 
527
+ אם יש שגיאות בשאילתות, ציין זאת בתשובה, אבל עדיין תן תשובה מפורטת על בסיס התוצאות שהתקבלו.
528
+
529
+ זכור: אתה מומחה תהליכי עבודה עסקיים דיגיטליים בעולם התוכן הממשלתי. התשובה שלך חייבת להיות מקצועית, רלוונטית, וברורה."""
530
 
531
  # Try Gemini first
532
  if settings.gemini_api_key and genai is not None:
 
564
  כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
565
  התשובה חייבת לענות ישירות על השא��ה: {query}
566
 
567
+ ⚠️ חובה קריטית - תשובה מילולית מפורטת:
568
+ - אתה חייב לכתוב תשובה מילולית מפורטת ומשמעותית - לא רק לרשום את השאילתות!
569
+ - התשובה חייבת להיות כתובה בשפה טבעית, מקצועית וקולחת
570
+ - התשובה חייבת להסביר את הממצאים, לא רק להציג אותם
571
  - אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
572
  - אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
573
  - אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
574
  - תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
575
 
576
+ דרישות מקצועיות:
577
+ 1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים) - חובה!
578
  2. תשובה שמתמקדת ישירות בשאלה שנשאלה
579
+ 3. כלול מספרים מדויקים מהתוצאות עם הסבר על המשמעות שלהם
580
+ 4. הסבר את המשמעות העסקית של הממצאים בהקשר של תהליכים דיגיטליים ממשלתיים
581
+ 5. כלול המלצות מעשיות לשיפור תהליכים ושירותים דיגיטליים
582
+ 6. כתוב בעברית מקצועית וקולחת, תוך שימוש במונחים מקצועיים מתחום הממשל הדיגיטלי
583
+
584
+ זכור: אתה מומחה תהליכי עבודה עסקיים דיגיטליים בעולם התוכן הממשלתי. התשובה שלך חייבת להיות מקצועית, רלוונטית, וברורה."""
585
 
586
  try:
587
  response = model.generate_content(improvement_prompt, generation_config=generation_config)
 
633
  כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
634
  התשובה חייבת לענות ישירות על השאלה: {query}
635
 
636
+ ⚠️ חובה קריטית - תשובה מילולית מפורטת:
637
+ - אתה חייב לכתוב תשובה מילולית מפורטת ומשמעותית - לא רק לרשום את השאילתות!
638
+ - התשובה חייבת להיות כתובה בשפה טבעית, מקצועית וקולחת
639
+ - התשובה חייבת להסביר את הממצאים, לא רק להציג אותם
640
  - אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
641
  - אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
642
  - אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
643
+ - תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
644
+
645
+ דרישות מקצועיות:
646
+ 1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים) - חובה!
647
+ 2. תשובה שמתמקדת ישירות בשאלה שנשאלה
648
+ 3. כלול מספרים מדויקים מהתוצאות עם הסבר על המשמעות שלהם
649
+ 4. הסבר את המשמעות העסקית של הממצאים בהקשר של תהליכים דיגיטליים ממשלתיים
650
+ 5. כלול המלצות מעשיות לשיפור תהליכים ושירותים דיגיטליים
651
+ 6. כתוב בעברית מקצועית וקולחת, תוך שימוש במונחים מקצועיים מתחום הממשל הדיגיטלי
652
+
653
+ זכור: אתה מומחה תהליכי עבודה עסקיים דיגיטליים בעולם התוכן הממשלתי. התשובה שלך חייבת להיות מקצועית, רלוונטית, וברורה."""
654
 
655
  try:
656
  response = client.chat.completions.create(
 
677
  # This ensures we always return a meaningful answer, not just a status message
678
  successful_results = [r for r in query_results if not r.error and len(r.result) > 0]
679
  if successful_results:
680
+ fallback_text = f"סיכום מפורט של הממצאים:\n\n"
681
+ fallback_text += f"בוצעו {len(sql_queries)} שאילתות, מתוכן {len(successful_results)} הצליחו והחזירו תוצאות.\n\n"
682
+
683
+ # Analyze and summarize each result
684
  for i, qr in enumerate(successful_results, 1):
685
+ fallback_text += f"ממצאים משאילתה {i}:\n"
686
+ fallback_text += f"שאילתה: {qr.query}\n"
687
+ fallback_text += f"מספר רשומות: {len(qr.result)}\n\n"
688
+
689
+ # Try to provide meaningful analysis
690
  if len(qr.result) > 0:
691
+ fallback_text += "תוצאות:\n"
692
+ # Show summary statistics if possible
693
+ numeric_cols = qr.result.select_dtypes(include=['number']).columns
694
+ if len(numeric_cols) > 0:
695
+ fallback_text += "סטטיסטיקות:\n"
696
+ for col in numeric_cols[:3]: # Limit to first 3 numeric columns
697
+ fallback_text += f"- {col}: ממוצע {qr.result[col].mean():.2f}, סכום {qr.result[col].sum():.0f}\n"
698
+ fallback_text += "\n"
699
+
700
+ # Show sample data
701
+ fallback_text += "דוגמאות מהנתונים:\n"
702
  fallback_text += qr.result.head(5).to_string(index=False)
703
  fallback_text += "\n\n"
704
+
705
+ fallback_text += "הערה: תשובה זו נוצרה אוטומטית מהתוצאות. לניתוח מפורט יותר, נסה לשאול שאלה ספציפית יותר."
706
  return fallback_text
707
  else:
708
  # If no successful results, still provide a helpful message
709
+ return f"בוצעו {len(sql_queries)} שאילתות, אך לא התקבלו תוצאות מהנתונים.\n\nייתכן שהנתונים לא מכילים מידע התואם לשאלה שנשאלה. נסה לשאול שאלה אחרת או לבדוק את הנתונים הזמינים."
710
 
711
  def _generate_visualizations(self, query_results: List[SQLQueryResult]) -> Optional[List[Dict[str, Any]]]:
712
  """
app/static/index.html CHANGED
@@ -88,21 +88,39 @@
88
  cursor: not-allowed;
89
  }
90
  .primary {
91
- background: linear-gradient(135deg, #0b63ff 0%, #0050cc 100%);
92
  color: white;
 
 
 
 
93
  }
94
  .primary:hover:not(:disabled) {
95
- background: linear-gradient(135deg, #0050cc 0%, #003d99 100%);
96
  transform: translateY(-3px);
97
- box-shadow: 0 6px 20px rgba(11,99,255,0.4);
 
 
 
 
 
98
  }
99
  .muted {
100
- background: #eef3ff;
101
- color: #0b2545;
 
 
 
102
  }
103
  .muted:hover {
104
- background: #dde6ff;
105
  transform: translateY(-2px);
 
 
 
 
 
 
106
  }
107
  .card {
108
  border-radius: 20px;
 
88
  cursor: not-allowed;
89
  }
90
  .primary {
91
+ background: linear-gradient(135deg, #1565c0 0%, #0d47a1 100%);
92
  color: white;
93
+ font-weight: 700;
94
+ text-shadow: 0 1px 2px rgba(0,0,0,0.2);
95
+ border: 2px solid #0d47a1;
96
+ box-shadow: 0 4px 12px rgba(13, 71, 161, 0.4), inset 0 1px 0 rgba(255,255,255,0.2);
97
  }
98
  .primary:hover:not(:disabled) {
99
+ background: linear-gradient(135deg, #0d47a1 0%, #01579b 100%);
100
  transform: translateY(-3px);
101
+ box-shadow: 0 8px 24px rgba(13, 71, 161, 0.5), inset 0 1px 0 rgba(255,255,255,0.2);
102
+ border-color: #01579b;
103
+ }
104
+ .primary:active:not(:disabled) {
105
+ transform: translateY(-1px);
106
+ box-shadow: 0 4px 12px rgba(13, 71, 161, 0.4);
107
  }
108
  .muted {
109
+ background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%);
110
+ color: #0d47a1;
111
+ font-weight: 600;
112
+ border: 2px solid #1976d2;
113
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.8);
114
  }
115
  .muted:hover {
116
+ background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
117
  transform: translateY(-2px);
118
+ box-shadow: 0 4px 16px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.8);
119
+ border-color: #1565c0;
120
+ }
121
+ .muted:active {
122
+ transform: translateY(0);
123
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
124
  }
125
  .card {
126
  border-radius: 20px;
scripts/fix_creation_date.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script to fix CreationDate column in Feedback.csv.
3
+
4
+ The CreationDate column contains values in MM:SS.s format (minutes:seconds.fraction),
5
+ which is not a valid date format. This script:
6
+ 1. Parses the MM:SS.s format
7
+ 2. Converts it to a proper datetime (assuming these are timestamps from a specific epoch)
8
+ 3. Saves the transformed data to feedback_transformed.csv
9
+
10
+ Since we don't have the actual date, we'll use a logical approach:
11
+ - Treat MM:SS.s as minutes:seconds since some reference point
12
+ - Convert to datetime by adding to a base date (e.g., 2020-01-01)
13
+ - This allows temporal queries to work correctly
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import pandas as pd
19
+ from datetime import datetime, timedelta
20
+ import re
21
+ from pathlib import Path
22
+
23
+
24
+ def parse_mmss_format(time_str: str) -> float | None:
25
+ """
26
+ Parse MM:SS.s format to total seconds.
27
+
28
+ Args:
29
+ time_str: String in format "MM:SS.s" (e.g., "21:56.3")
30
+
31
+ Returns:
32
+ Total seconds as float, or None if parsing fails
33
+ """
34
+ if pd.isna(time_str) or not isinstance(time_str, str):
35
+ return None
36
+
37
+ # Match pattern MM:SS.s or MM:SS
38
+ match = re.match(r'^(\d+):(\d+)\.?(\d*)$', time_str.strip())
39
+ if not match:
40
+ return None
41
+
42
+ minutes = int(match.group(1))
43
+ seconds = int(match.group(2))
44
+ fraction = float(f"0.{match.group(3)}") if match.group(3) else 0.0
45
+
46
+ total_seconds = minutes * 60 + seconds + fraction
47
+ return total_seconds
48
+
49
+
50
+ def convert_to_datetime(seconds: float | None, index: int, total_rows: int,
51
+ base_date: datetime = datetime(2020, 1, 1),
52
+ period_days: int = 365) -> datetime | None:
53
+ """
54
+ Convert seconds to datetime by distributing records over a time period.
55
+
56
+ Since the original MM:SS.s values don't contain actual date information,
57
+ we distribute the records evenly over a period (default: 1 year) and use
58
+ the MM:SS.s value as a time-of-day component.
59
+
60
+ Args:
61
+ seconds: Total seconds from MM:SS.s format (used as time-of-day)
62
+ index: Row index (0-based)
63
+ total_rows: Total number of rows
64
+ base_date: Base date to start from
65
+ period_days: Number of days to distribute records over
66
+
67
+ Returns:
68
+ Datetime object or None
69
+ """
70
+ if seconds is None:
71
+ return None
72
+
73
+ try:
74
+ # Distribute records evenly over the period
75
+ days_offset = (index / total_rows) * period_days
76
+
77
+ # Use the seconds as time-of-day (hours, minutes, seconds)
78
+ hours = int(seconds // 3600)
79
+ minutes = int((seconds % 3600) // 60)
80
+ secs = int(seconds % 60)
81
+ microseconds = int((seconds % 1) * 1000000)
82
+
83
+ # Calculate the date
84
+ target_date = base_date + timedelta(days=days_offset)
85
+
86
+ # Set the time component
87
+ return target_date.replace(hour=hours % 24, minute=minutes % 60,
88
+ second=secs % 60, microsecond=microseconds)
89
+ except Exception:
90
+ return None
91
+
92
+
93
+ def fix_creation_date(input_csv: str = "Feedback.csv", output_csv: str = "feedback_transformed.csv") -> None:
94
+ """
95
+ Fix CreationDate column and save transformed CSV.
96
+
97
+ Args:
98
+ input_csv: Path to input CSV file
99
+ output_csv: Path to output CSV file
100
+ """
101
+ print(f"Loading {input_csv}...")
102
+ df = pd.read_csv(input_csv)
103
+
104
+ print(f"Original shape: {df.shape}")
105
+ print(f"Original CreationDate sample: {df['CreationDate'].head(5).tolist()}")
106
+
107
+ if 'CreationDate' not in df.columns:
108
+ print("Warning: CreationDate column not found!")
109
+ return
110
+
111
+ # Parse MM:SS.s format to seconds
112
+ print("Parsing CreationDate values...")
113
+ df['_seconds'] = df['CreationDate'].apply(parse_mmss_format)
114
+
115
+ # Check if we have valid values
116
+ valid_count = df['_seconds'].notna().sum()
117
+ print(f"Valid parsed values: {valid_count} / {len(df)}")
118
+
119
+ if valid_count == 0:
120
+ print("Error: No valid CreationDate values found!")
121
+ return
122
+
123
+ # Convert to datetime
124
+ # Distribute records over a 1-year period starting from 2020-01-01
125
+ # Use the MM:SS.s value as time-of-day component
126
+ base_date = datetime(2020, 1, 1, 0, 0, 0)
127
+ total_rows = len(df)
128
+
129
+ print(f"Converting to datetime (distributing over 1 year from {base_date.date()})...")
130
+ df['CreationDate'] = [
131
+ convert_to_datetime(seconds, idx, total_rows, base_date, period_days=365)
132
+ for idx, seconds in enumerate(df['_seconds'])
133
+ ]
134
+
135
+ # Convert to string format for CSV
136
+ df['CreationDate'] = df['CreationDate'].apply(
137
+ lambda x: x.strftime('%Y-%m-%d %H:%M:%S') if pd.notna(x) else None
138
+ )
139
+
140
+ # Drop temporary column
141
+ df = df.drop(columns=['_seconds'])
142
+
143
+ # Save transformed CSV
144
+ print(f"Saving transformed data to {output_csv}...")
145
+ df.to_csv(output_csv, index=False)
146
+
147
+ print(f"Transformed shape: {df.shape}")
148
+ print(f"New CreationDate sample: {df['CreationDate'].head(5).tolist()}")
149
+ print(f"CreationDate range: {df['CreationDate'].min()} to {df['CreationDate'].max()}")
150
+ print(f"✅ Successfully created {output_csv}")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ fix_creation_date()
155
+