a-dabs commited on
Commit
8d72f48
·
verified ·
1 Parent(s): b14540f

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. .gitattributes +8 -0
  3. .github/workflows/update_space.yml +28 -0
  4. .gradio/certificate.pem +31 -0
  5. .vector_services/usiu_vector_db/chroma.sqlite3 +3 -0
  6. README.md +3 -9
  7. api_gateway/__init__.py +0 -0
  8. api_gateway/__pycache__/__init__.cpython-311.pyc +0 -0
  9. api_gateway/__pycache__/gateway.cpython-311.pyc +0 -0
  10. api_gateway/gateway.py +128 -0
  11. app.py +41 -0
  12. configs/__pycache__/config.cpython-311.pyc +0 -0
  13. configs/config.py +24 -0
  14. images/2D & 3D Plots of vectors 2.png +3 -0
  15. images/2D & 3D Plots of vectors.png +3 -0
  16. images/2D Vectors.png +3 -0
  17. images/3D Vectors 2.png +3 -0
  18. images/3D Vectors.png +3 -0
  19. images/usiu-logo.png +0 -0
  20. main.py +70 -0
  21. requirements.txt +564 -0
  22. requirements2.txt +10 -0
  23. services/__init__.py +0 -0
  24. services/__pycache__/__init__.cpython-311.pyc +0 -0
  25. services/__pycache__/auth_service.cpython-311.pyc +0 -0
  26. services/__pycache__/chatbot_rag.cpython-311.pyc +0 -0
  27. services/__pycache__/data_service.cpython-311.pyc +0 -0
  28. services/__pycache__/file_service.cpython-311.pyc +0 -0
  29. services/__pycache__/general_chat_service.cpython-311.pyc +0 -0
  30. services/__pycache__/response_service.cpython-311.pyc +0 -0
  31. services/__pycache__/study_support_service.cpython-311.pyc +0 -0
  32. services/__pycache__/system_prompts_service.cpython-311.pyc +0 -0
  33. services/__pycache__/ui_service.cpython-311.pyc +0 -0
  34. services/__pycache__/voice_interface.cpython-311.pyc +0 -0
  35. services/auth_service.py +132 -0
  36. services/data_service-old1.py +57 -0
  37. services/data_service.py +14 -0
  38. services/file_service.py +168 -0
  39. services/quantized_model_loader.py +67 -0
  40. services/system_prompts_service.py +15 -0
  41. services/ui_service-old2.py +944 -0
  42. services/ui_service-old3.py +1527 -0
  43. services/ui_service.py +1560 -0
  44. speech.wav +0 -0
  45. system_prompts/__init__.py +0 -0
  46. system_prompts/__pycache__/__init__.cpython-311.pyc +0 -0
  47. system_prompts/__pycache__/prompt_for_general_chat.cpython-311.pyc +0 -0
  48. system_prompts/__pycache__/prompt_for_study_support.cpython-311.pyc +0 -0
  49. system_prompts/__pycache__/prompts_manager.cpython-311.pyc +0 -0
  50. system_prompts/__pycache__/system_prompts.cpython-311.pyc +0 -0
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.gitattributes CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ .vector_services/usiu_vector_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
37
+ images/2D[[:space:]]&[[:space:]]3D[[:space:]]Plots[[:space:]]of[[:space:]]vectors[[:space:]]2.png filter=lfs diff=lfs merge=lfs -text
38
+ images/2D[[:space:]]&[[:space:]]3D[[:space:]]Plots[[:space:]]of[[:space:]]vectors.png filter=lfs diff=lfs merge=lfs -text
39
+ images/2D[[:space:]]Vectors.png filter=lfs diff=lfs merge=lfs -text
40
+ images/3D[[:space:]]Vectors[[:space:]]2.png filter=lfs diff=lfs merge=lfs -text
41
+ images/3D[[:space:]]Vectors.png filter=lfs diff=lfs merge=lfs -text
42
+ usiu_vector_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
43
+ vector_services/usiu_vector_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
.github/workflows/update_space.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Run Python script
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v2
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v2
18
+ with:
19
+ python-version: '3.9'
20
+
21
+ - name: Install Gradio
22
+ run: python -m pip install gradio
23
+
24
+ - name: Log in to Hugging Face
25
+ run: python -c 'import huggingface_hub; huggingface_hub.login(token="${{ secrets.hf_token }}")'
26
+
27
+ - name: Deploy to Spaces
28
+ run: gradio deploy
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
.vector_services/usiu_vector_db/chroma.sqlite3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7f4d32541e51737af91299dcfae2e6c1919587a129154c993cc0fa7c9cc4096
3
+ size 167936
README.md CHANGED
@@ -1,12 +1,6 @@
1
  ---
2
- title: Chatbot Prototype
3
- emoji: 🐠
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 5.25.0
8
  app_file: app.py
9
- pinned: false
 
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: chatbot_prototype
 
 
 
 
 
3
  app_file: app.py
4
+ sdk: gradio
5
+ sdk_version: 5.12.0
6
  ---
 
 
api_gateway/__init__.py ADDED
File without changes
api_gateway/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (263 Bytes). View file
 
api_gateway/__pycache__/gateway.cpython-311.pyc ADDED
Binary file (8.31 kB). View file
 
api_gateway/gateway.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # api_gateway/gateway.py
2
+
3
+ from typing import Dict, Any, Optional, List, Union, Callable
4
+ import time
5
+ import threading
6
+ import logging
7
+
8
+ # Configure logging
9
+ logging.basicConfig(level=logging.INFO,
10
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
11
+ logger = logging.getLogger('api_gateway')
12
+
13
+ class APIGateway:
14
+ """
15
+ A simple API Gateway that routes requests to appropriate services.
16
+ Implements service discovery, request routing, and basic monitoring.
17
+ """
18
+
19
+ def __init__(self):
20
+ # Registry of available services
21
+ self._services = {}
22
+
23
+ # Request metrics
24
+ self._request_count = 0
25
+ self._service_metrics = {}
26
+ self._lock = threading.Lock()
27
+
28
+ # Start metrics reporting
29
+ self._start_metrics_reporting()
30
+
31
+ logger.info("API Gateway initialized")
32
+
33
+ def register_service(self, service_name: str, service_instance: Any) -> None:
34
+ """Register a service with the gateway"""
35
+ self._services[service_name] = service_instance
36
+ self._service_metrics[service_name] = {
37
+ "requests": 0,
38
+ "errors": 0,
39
+ "avg_response_time": 0
40
+ }
41
+ logger.info(f"Registered service: {service_name}")
42
+
43
+ def get_service(self, service_name: str) -> Optional[Any]:
44
+ """Get a service instance by name"""
45
+ return self._services.get(service_name)
46
+
47
+ def list_services(self) -> List[str]:
48
+ """List all registered services"""
49
+ return list(self._services.keys())
50
+
51
+ def route_request(self, service_name: str, method_name: str, *args, **kwargs) -> Any:
52
+ """
53
+ Route a request to the appropriate service method
54
+ Handles metrics collection and error handling
55
+ """
56
+ service = self.get_service(service_name)
57
+ if not service:
58
+ logger.error(f"Service not found: {service_name}")
59
+ raise ValueError(f"Service '{service_name}' not found")
60
+
61
+ method = getattr(service, method_name, None)
62
+ if not method or not callable(method):
63
+ logger.error(f"Method not found: {service_name}.{method_name}")
64
+ raise ValueError(f"Method '{method_name}' not found in service '{service_name}'")
65
+
66
+ # Track request metrics
67
+ with self._lock:
68
+ self._request_count += 1
69
+ self._service_metrics[service_name]["requests"] += 1
70
+
71
+ # Execute the request with timing
72
+ start_time = time.time()
73
+ try:
74
+ result = method(*args, **kwargs)
75
+ return result
76
+ except Exception as e:
77
+ logger.error(f"Error routing request to {service_name}.{method_name}: {str(e)}")
78
+ with self._lock:
79
+ self._service_metrics[service_name]["errors"] += 1
80
+ raise
81
+ finally:
82
+ # Calculate response time
83
+ response_time = time.time() - start_time
84
+
85
+ # Update average response time
86
+ with self._lock:
87
+ metrics = self._service_metrics[service_name]
88
+ total_requests = metrics["requests"]
89
+ current_avg = metrics["avg_response_time"]
90
+
91
+ # Calculate new rolling average
92
+ if total_requests > 1:
93
+ new_avg = ((current_avg * (total_requests - 1)) + response_time) / total_requests
94
+ else:
95
+ new_avg = response_time
96
+
97
+ metrics["avg_response_time"] = new_avg
98
+
99
+ def _start_metrics_reporting(self) -> None:
100
+ """Start a background thread to periodically report metrics"""
101
+ def report_metrics():
102
+ while True:
103
+ time.sleep(60) # Report every minute
104
+ with self._lock:
105
+ logger.info(f"Total requests processed: {self._request_count}")
106
+ for service, metrics in self._service_metrics.items():
107
+ logger.info(f"Service: {service}, "
108
+ f"Requests: {metrics['requests']}, "
109
+ f"Errors: {metrics['errors']}, "
110
+ f"Avg Response Time: {metrics['avg_response_time']:.4f}s")
111
+
112
+ thread = threading.Thread(target=report_metrics, daemon=True)
113
+ thread.start()
114
+
115
+ # Create a singleton instance
116
+ gateway = APIGateway()
117
+
118
+ def register_service(service_name: str, service_instance: Any) -> None:
119
+ """Register a service with the gateway"""
120
+ gateway.register_service(service_name, service_instance)
121
+
122
+ def get_service(service_name: str) -> Optional[Any]:
123
+ """Get a service instance by name"""
124
+ return gateway.get_service(service_name)
125
+
126
+ def route_request(service_name: str, method_name: str, *args, **kwargs) -> Any:
127
+ """Route a request to a service method"""
128
+ return gateway.route_request(service_name, method_name, *args, **kwargs)
app.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ import os
3
+ import gradio as gr
4
+ from services.data_service import DataService
5
+ from services.ui_service import dashboard_ui
6
+ from configs.config import GPT4O_MODEL, CLAUDE_MODEL
7
+ from vector_services.data_curator import DataCurator
8
+ from api_gateway.gateway import register_service, gateway
9
+
10
+ def main() -> None:
11
+ base_dir = "./usiu-knowledge-base"
12
+ vector_store_dir = "week5/chatbot_prototype/vector_services/usiu_vector_db"
13
+
14
+ # Load the existing vector store and obtain its retriever
15
+ curator = DataCurator(knowledge_base_dir=base_dir, persist_directory=vector_store_dir)
16
+ vector_store = curator.load_vectorstore()
17
+ retriever = curator.get_retriever()
18
+
19
+ # Initialize DataService with the retriever so that general chat uses RAG
20
+ data_service = DataService(retriever=retriever)
21
+
22
+ # Register services with API Gateway
23
+ register_service("data_service", data_service)
24
+ register_service("curator", curator)
25
+
26
+ # Get system prompts for general and study support
27
+ general_chat_prompt, study_prompt = data_service.prompts_service.get_prompt()
28
+
29
+ # Build the dashboard, passing the relevant prompts, model identifiers, and retriever
30
+ dashboard = dashboard_ui(
31
+ general_chat_prompt=general_chat_prompt,
32
+ general_model=GPT4O_MODEL,
33
+ study_prompt=study_prompt,
34
+ study_model=CLAUDE_MODEL,
35
+ retriever=retriever # Pass the retriever directly to ui_service
36
+ )
37
+
38
+ dashboard.launch(share=True, inbrowser=True, server_name="0.0.0.0")
39
+
40
+ if __name__ == "__main__":
41
+ main()
configs/__pycache__/config.cpython-311.pyc ADDED
Binary file (946 Bytes). View file
 
configs/config.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # configs/config.py
2
+ import os
3
+ import anthropic
4
+ from dotenv import load_dotenv
5
+
6
+ # Load environment variables from .env file.
7
+ load_dotenv()
8
+
9
+ claude = anthropic.Anthropic()
10
+
11
+ # LLM Model Settings
12
+ GPT4O_MODEL = "gpt-4o"
13
+ CLAUDE_MODEL = "claude-3-7-sonnet-20250219"
14
+ LLAMA_MODEL = "meta-llama/Meta-Llama-3-8B"
15
+
16
+ # API Keys
17
+ GPT4O_API_KEY = os.getenv('OPENAI_API_KEY')
18
+ CLAUDE_API_KEY = os.getenv('ANTHROPIC_API_KEY')
19
+ LLAMA_API_KEY = os.getenv("HF_TOKEN")
20
+
21
+ # Scraping parameters
22
+ SCRAPER_MAX_DEPTH = 3
23
+ SCRAPER_MAX_PAGES = 200
24
+ MIN_CONTENT_LENGTH = 200
images/2D & 3D Plots of vectors 2.png ADDED

Git LFS Details

  • SHA256: 93f89c9ca80e5e5329e99549f50a5179288c01eda21e5264a53576b1795ee7da
  • Pointer size: 131 Bytes
  • Size of remote file: 379 kB
images/2D & 3D Plots of vectors.png ADDED

Git LFS Details

  • SHA256: 82dfc115bc2f9adb05092dfb453ec9641b13c16d4c3de39277773fddbdf40857
  • Pointer size: 131 Bytes
  • Size of remote file: 328 kB
images/2D Vectors.png ADDED

Git LFS Details

  • SHA256: a351f058446479713b0c98183444b31c63c86123a9e4936a8dbc229637c7af6a
  • Pointer size: 131 Bytes
  • Size of remote file: 190 kB
images/3D Vectors 2.png ADDED

Git LFS Details

  • SHA256: d247f28d0163fd811abd6ab2e2ec6e3828b7b82bae2da7502618b243a9e0325d
  • Pointer size: 131 Bytes
  • Size of remote file: 232 kB
images/3D Vectors.png ADDED

Git LFS Details

  • SHA256: a7fc7b0b069a519a9e7c5288515bdacef7e8f497affe78e93b2b554bd3607ef2
  • Pointer size: 131 Bytes
  • Size of remote file: 180 kB
images/usiu-logo.png ADDED
main.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # #!/usr/bin/env python
2
+ # import os
3
+ # import gradio as gr
4
+ # from services.data_service import DataService
5
+ # from services.ui_service import general_chat_ui, study_support_ui
6
+ # from configs.config import GPT4O_MODEL, CLAUDE_MODEL
7
+ # from vector_services.data_curator import DataCurator
8
+
9
+ # def main() -> None:
10
+ # base_dir = "./usiu-knowledge-base"
11
+ # vector_store_dir = "./usiu_vector_db"
12
+
13
+ # # Load the existing vector store and obtain its retriever.
14
+ # curator = DataCurator(knowledge_base_dir=base_dir, persist_directory=vector_store_dir)
15
+ # vector_store = curator.load_vectorstore()
16
+ # retriever = curator.get_retriever()
17
+
18
+ # # Initialize DataService with the retriever so that general chat uses RAG.
19
+ # data_service = DataService(retriever=retriever)
20
+ # prompts_service = data_service.prompts_service
21
+
22
+ # # Get system prompts for general and study support.
23
+ # general_chat_prompt = prompts_service.get_prompt("general")
24
+ # study_prompt = prompts_service.get_prompt("study")
25
+
26
+ # # Use the ui_service functions that already manage state properly.
27
+ # general_ui = general_chat_ui(general_chat_prompt, GPT4O_MODEL)
28
+ # study_ui = study_support_ui(study_prompt, CLAUDE_MODEL)
29
+
30
+ # # Assemble the interfaces in tabs.
31
+ # interfaces = [general_ui, study_ui]
32
+ # tab_names = ["General Academic Chat", "Study Support Chat"]
33
+ # demo = gr.TabbedInterface(interfaces, tab_names)
34
+ # demo.launch(share=True, inbrowser=True, server_name="localhost", server_port=8001)
35
+
36
+ # if __name__ == "__main__":
37
+ # main()
38
+
39
+
40
+ #!/usr/bin/env python
41
+ import os
42
+ import gradio as gr
43
+ from services.data_service import DataService
44
+ from services.ui_service import dashboard_ui
45
+ from configs.config import GPT4O_MODEL, CLAUDE_MODEL
46
+ from vector_services.data_curator import DataCurator
47
+
48
+ def main() -> None:
49
+ base_dir = "./usiu-knowledge-base"
50
+ vector_store_dir = "./vector_services/usiu_vector_db"
51
+
52
+ # Load the existing vector store and obtain its retriever.
53
+ curator = DataCurator(knowledge_base_dir=base_dir, persist_directory=vector_store_dir)
54
+ vector_store = curator.load_vectorstore()
55
+ retriever = curator.get_retriever()
56
+
57
+ # Initialize DataService with the retriever so that general chat uses RAG.
58
+ data_service = DataService(retriever=retriever)
59
+ prompts_service = data_service.prompts_service
60
+
61
+ # Get system prompts for general and study support.
62
+ general_chat_prompt, study_prompt = prompts_service.get_prompt()
63
+
64
+ # Build the dashboard, passing the relevant prompts and model identifiers.
65
+ dashboard = dashboard_ui(general_chat_prompt, GPT4O_MODEL, study_prompt, CLAUDE_MODEL)
66
+ dashboard.launch(share=True, inbrowser=True, server_name="0.0.0.0")
67
+
68
+ if __name__ == "__main__":
69
+ main()
70
+
requirements.txt ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ absl-py==2.2.1
2
+ accelerate==0.21.0
3
+ aiofiles @ file:///home/conda/feedstock_root/build_artifacts/aiofiles_1698945915105/work
4
+ aiohappyeyeballs @ file:///home/conda/feedstock_root/build_artifacts/aiohappyeyeballs_1733331917983/work
5
+ aiohttp==3.11.11
6
+ aiohttp-retry==2.8.3
7
+ aiosignal @ file:///home/conda/feedstock_root/build_artifacts/aiosignal_1734342155601/work
8
+ altair==5.5.0
9
+ annotated-types @ file:///home/conda/feedstock_root/build_artifacts/annotated-types_1733247046149/work
10
+ ansi2html @ file:///Users/runner/miniforge3/conda-bld/ansi2html_1726555528398/work
11
+ anthropic==0.45.2
12
+ anyascii==0.3.2
13
+ anyio @ file:///home/conda/feedstock_root/build_artifacts/anyio_1736174388474/work
14
+ appdirs==1.4.4
15
+ appnope @ file:///home/conda/feedstock_root/build_artifacts/appnope_1733332318622/work
16
+ argon2-cffi @ file:///home/conda/feedstock_root/build_artifacts/argon2-cffi_1733311059102/work
17
+ argon2-cffi-bindings @ file:///Users/runner/miniforge3/conda-bld/argon2-cffi-bindings_1725356591756/work
18
+ arrow @ file:///home/conda/feedstock_root/build_artifacts/arrow_1733584251875/work
19
+ asgiref @ file:///home/conda/feedstock_root/build_artifacts/asgiref_1733215607532/work
20
+ asttokens @ file:///home/conda/feedstock_root/build_artifacts/asttokens_1733250440834/work
21
+ async-lru @ file:///home/conda/feedstock_root/build_artifacts/async-lru_1733584297267/work
22
+ attrs @ file:///home/conda/feedstock_root/build_artifacts/attrs_1734348785146/work
23
+ audioread==3.0.1
24
+ babel @ file:///home/conda/feedstock_root/build_artifacts/babel_1733236348445/work
25
+ backoff @ file:///home/conda/feedstock_root/build_artifacts/backoff_1733771078379/work
26
+ bangla==0.0.2
27
+ bcrypt @ file:///Users/runner/miniforge3/conda-bld/bcrypt_1732074870204/work
28
+ beautifulsoup4==4.12.2
29
+ bitsandbytes==0.42.0
30
+ bleach @ file:///home/conda/feedstock_root/build_artifacts/bleach_1736148489770/work
31
+ blinker @ file:///home/conda/feedstock_root/build_artifacts/blinker_1731096409132/work
32
+ blis==1.2.0
33
+ bnnumerizer==0.0.2
34
+ bnunicodenormalizer==0.1.7
35
+ Brotli @ file:///Users/runner/miniforge3/conda-bld/brotli-split_1725267563793/work
36
+ build @ file:///home/conda/feedstock_root/build_artifacts/python-build_1733230610871/work
37
+ cached-property @ file:///home/conda/feedstock_root/build_artifacts/cached_property_1615209429212/work
38
+ cachetools @ file:///home/conda/feedstock_root/build_artifacts/cachetools_1737517575301/work
39
+ catalogue==2.0.10
40
+ certifi @ file:///home/conda/feedstock_root/build_artifacts/certifi_1734380492396/work/certifi
41
+ cffi @ file:///Users/runner/miniforge3/conda-bld/cffi_1725560551097/work
42
+ charset-normalizer==2.1.1
43
+ chroma-hnswlib @ file:///Users/runner/miniforge3/conda-bld/chroma-hnswlib_1725527880077/work
44
+ chromadb @ file:///Users/runner/miniforge3/conda-bld/chromadb_1736965865001/work
45
+ chromedriver-autoinstaller==0.6.4
46
+ click @ file:///home/conda/feedstock_root/build_artifacts/click_1734858813237/work
47
+ clickhouse-connect @ file:///Users/runner/miniforge3/conda-bld/clickhouse-connect_1736287047793/work
48
+ cloudpathlib==0.21.0
49
+ colorama @ file:///home/conda/feedstock_root/build_artifacts/colorama_1733218098505/work
50
+ coloredlogs @ file:///home/conda/feedstock_root/build_artifacts/coloredlogs_1733927977068/work
51
+ comm @ file:///home/conda/feedstock_root/build_artifacts/comm_1733502965406/work
52
+ confection==0.1.5
53
+ contourpy @ file:///Users/runner/miniforge3/conda-bld/contourpy_1731428370657/work
54
+ coqpit==0.0.17
55
+ cryptography @ file:///Users/runner/miniforge3/conda-bld/cryptography-split_1737764929298/work
56
+ cycler @ file:///home/conda/feedstock_root/build_artifacts/cycler_1733332471406/work
57
+ cymem==2.0.11
58
+ Cython==3.0.12
59
+ dash @ file:///home/conda/feedstock_root/build_artifacts/dash_1734247674276/work
60
+ dataclasses-json==0.5.14
61
+ datasets==2.14.5
62
+ dateparser==1.1.8
63
+ debugpy @ file:///Users/runner/miniforge3/conda-bld/debugpy_1734158991118/work
64
+ decorator @ file:///home/conda/feedstock_root/build_artifacts/decorator_1733236420667/work
65
+ deepseek==1.0.0
66
+ defusedxml @ file:///home/conda/feedstock_root/build_artifacts/defusedxml_1615232257335/work
67
+ Deprecated @ file:///home/conda/feedstock_root/build_artifacts/deprecated_1737986966356/work
68
+ dill==0.3.7
69
+ distro==1.9.0
70
+ dnspython @ file:///home/conda/feedstock_root/build_artifacts/dnspython_1733256735222/work
71
+ docker-pycreds==0.4.0
72
+ docopt==0.6.2
73
+ duckdb @ file:///Users/runner/miniforge3/conda-bld/python-duckdb-split_1730798348017/work/tools/pythonpkg
74
+ durationpy @ file:///home/conda/feedstock_root/build_artifacts/durationpy_1734343542351/work
75
+ einops==0.8.1
76
+ email_validator @ file:///home/conda/feedstock_root/build_artifacts/email-validator-meta_1733300719943/work
77
+ encodec==0.1.1
78
+ entrypoints @ file:///home/conda/feedstock_root/build_artifacts/entrypoints_1733327148154/work
79
+ exceptiongroup @ file:///home/conda/feedstock_root/build_artifacts/exceptiongroup_1733208806608/work
80
+ executing @ file:///home/conda/feedstock_root/build_artifacts/executing_1733569351617/work
81
+ faiss==1.9.0
82
+ faiss-cpu==1.10.0
83
+ fastapi @ file:///home/conda/feedstock_root/build_artifacts/fastapi_1733362299471/work
84
+ fastapi-cli @ file:///home/conda/feedstock_root/build_artifacts/fastapi-cli_1734302308128/work
85
+ fastjsonschema @ file:///home/conda/feedstock_root/build_artifacts/python-fastjsonschema_1733235979760/work/dist
86
+ feedparser==6.0.11
87
+ ffmpy @ file:///home/conda/feedstock_root/build_artifacts/ffmpy_1659474992694/work
88
+ filelock @ file:///home/conda/feedstock_root/build_artifacts/filelock_1733240801289/work
89
+ Flask==2.2.2
90
+ flatbuffers @ file:///home/conda/feedstock_root/build_artifacts/python-flatbuffers_1733838640534/work
91
+ fonttools @ file:///Users/runner/miniforge3/conda-bld/fonttools_1735335860711/work
92
+ fqdn @ file:///home/conda/feedstock_root/build_artifacts/fqdn_1733327382592/work/dist
93
+ frozenlist @ file:///Users/runner/miniforge3/conda-bld/frozenlist_1737645277522/work
94
+ fsspec==2023.6.0
95
+ g2pkk==0.1.2
96
+ gensim==4.3.3
97
+ gitdb==4.0.12
98
+ GitPython==3.1.44
99
+ gmpy2 @ file:///Users/runner/miniforge3/conda-bld/gmpy2_1733462580180/work
100
+ google-ai-generativelanguage==0.6.15
101
+ google-api-core==2.24.0
102
+ google-api-python-client==2.158.0
103
+ google-auth @ file:///home/conda/feedstock_root/build_artifacts/google-auth_1737618250101/work
104
+ google-auth-httplib2==0.2.0
105
+ google-generativeai==0.8.4
106
+ googleapis-common-protos @ file:///home/conda/feedstock_root/build_artifacts/googleapis-common-protos-feedstock_1731458889232/work
107
+ gradio==5.23.1
108
+ gradio_client==1.8.0
109
+ groovy==0.1.2
110
+ groq==0.18.0
111
+ grpcio==1.69.0
112
+ grpcio-status==1.69.0
113
+ grpclib==0.4.7
114
+ gruut==2.2.3
115
+ gruut-ipa==0.13.0
116
+ gruut_lang_de==2.0.1
117
+ gruut_lang_en==2.0.1
118
+ gruut_lang_es==2.0.1
119
+ gruut_lang_fr==2.0.2
120
+ gTTS==2.5.4
121
+ h11 @ file:///home/conda/feedstock_root/build_artifacts/h11_1733327467879/work
122
+ h2 @ file:///home/conda/feedstock_root/build_artifacts/h2_1733298745555/work
123
+ hangul-romanize==0.1.0
124
+ hnswlib @ file:///Users/runner/miniforge3/conda-bld/hnswlib_1732136479184/work
125
+ hpack @ file:///home/conda/feedstock_root/build_artifacts/hpack_1733299205993/work
126
+ httpcore @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_httpcore_1731707562/work
127
+ httplib2==0.22.0
128
+ httptools @ file:///Users/runner/miniforge3/conda-bld/httptools_1732707694945/work
129
+ httpx==0.27.2
130
+ httpx-sse==0.4.0
131
+ huggingface-hub==0.29.3
132
+ humanfriendly @ file:///home/conda/feedstock_root/build_artifacts/humanfriendly_1733927922002/work
133
+ hyperframe @ file:///home/conda/feedstock_root/build_artifacts/hyperframe_1733298771451/work
134
+ idna @ file:///home/conda/feedstock_root/build_artifacts/idna_1733211830134/work
135
+ importlib_metadata @ file:///home/conda/feedstock_root/build_artifacts/importlib-metadata_1721856510709/work
136
+ importlib_resources @ file:///home/conda/feedstock_root/build_artifacts/importlib_resources_1736252299705/work
137
+ inflect==7.5.0
138
+ ipykernel @ file:///Users/runner/miniforge3/conda-bld/ipykernel_1719845458456/work
139
+ ipython @ file:///home/conda/feedstock_root/build_artifacts/ipython_1734788142186/work
140
+ ipywidgets @ file:///home/conda/feedstock_root/build_artifacts/ipywidgets_1733493556527/work
141
+ isoduration @ file:///home/conda/feedstock_root/build_artifacts/isoduration_1733493628631/work/dist
142
+ itsdangerous @ file:///home/conda/feedstock_root/build_artifacts/itsdangerous_1733308265247/work
143
+ jamo==0.4.1
144
+ jedi @ file:///home/conda/feedstock_root/build_artifacts/jedi_1733300866624/work
145
+ jieba==0.42.1
146
+ Jinja2 @ file:///home/conda/feedstock_root/build_artifacts/jinja2_1734823942230/work
147
+ jiter==0.8.2
148
+ jiwer==3.1.0
149
+ joblib @ file:///home/conda/feedstock_root/build_artifacts/joblib_1733736026804/work
150
+ json5 @ file:///home/conda/feedstock_root/build_artifacts/json5_1733272076743/work
151
+ jsonlines==1.2.0
152
+ jsonpatch==1.33
153
+ jsonpointer @ file:///Users/runner/miniforge3/conda-bld/jsonpointer_1725302946874/work
154
+ jsonschema @ file:///home/conda/feedstock_root/build_artifacts/jsonschema_1733472696581/work
155
+ jsonschema-specifications @ file:///tmp/tmpk0f344m9/src
156
+ jupyter-dash @ file:///home/conda/feedstock_root/build_artifacts/jupyter-dash_1648919001274/work
157
+ jupyter-events @ file:///home/conda/feedstock_root/build_artifacts/jupyter_events_1734531682843/work
158
+ jupyter-lsp @ file:///home/conda/feedstock_root/build_artifacts/jupyter-lsp-meta_1733492907176/work/jupyter-lsp
159
+ jupyter_client @ file:///home/conda/feedstock_root/build_artifacts/jupyter_client_1733440914442/work
160
+ jupyter_core @ file:///home/conda/feedstock_root/build_artifacts/jupyter_core_1727163409502/work
161
+ jupyter_server @ file:///home/conda/feedstock_root/build_artifacts/jupyter_server_1734702637701/work
162
+ jupyter_server_terminals @ file:///home/conda/feedstock_root/build_artifacts/jupyter_server_terminals_1733427956852/work
163
+ jupyterlab @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_1738184697351/work
164
+ jupyterlab_pygments @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_pygments_1733328101776/work
165
+ jupyterlab_server @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_server_1733599573484/work
166
+ jupyterlab_widgets @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_widgets_1733428046021/work
167
+ kiwisolver @ file:///Users/runner/miniforge3/conda-bld/kiwisolver_1725459112978/work
168
+ kubernetes @ file:///home/conda/feedstock_root/build_artifacts/python-kubernetes_1737737545868/work
169
+ langchain==0.3.4
170
+ langchain-chroma==0.2.1
171
+ langchain-community==0.3.3
172
+ langchain-core==0.3.34
173
+ langchain-openai==0.3.4
174
+ langchain-text-splitters==0.3.6
175
+ langchainplus-sdk==0.0.20
176
+ langcodes==3.5.0
177
+ langsmith==0.1.147
178
+ language_data==1.3.0
179
+ lazy_loader==0.4
180
+ librosa==0.10.2.post1
181
+ linkify-it-py==2.0.3
182
+ llvmlite==0.44.0
183
+ lz4 @ file:///Users/runner/miniforge3/conda-bld/lz4_1725089476490/work
184
+ marisa-trie==1.2.1
185
+ Markdown==3.7
186
+ markdown-it-py==2.2.0
187
+ MarkupSafe @ file:///Users/runner/miniforge3/conda-bld/markupsafe_1724959500379/work
188
+ marshmallow==3.25.0
189
+ matplotlib==3.7.2
190
+ matplotlib-inline @ file:///home/conda/feedstock_root/build_artifacts/matplotlib-inline_1733416936468/work
191
+ mdit-py-plugins==0.3.3
192
+ mdurl @ file:///home/conda/feedstock_root/build_artifacts/mdurl_1733255585584/work
193
+ mistune @ file:///home/conda/feedstock_root/build_artifacts/mistune_1735686876364/work
194
+ mmh3 @ file:///Users/runner/miniforge3/conda-bld/mmh3_1737968080672/work
195
+ modal==0.73.126
196
+ monotonic @ file:///home/conda/feedstock_root/build_artifacts/monotonic_1734029696393/work
197
+ more-itertools==10.6.0
198
+ mpmath @ file:///home/conda/feedstock_root/build_artifacts/mpmath_1733302684489/work
199
+ msgpack==1.1.0
200
+ multidict @ file:///Users/runner/miniforge3/conda-bld/multidict_1729065505264/work
201
+ multiprocess==0.70.15
202
+ munkres==1.1.4
203
+ murmurhash==1.0.12
204
+ mypy-extensions==1.0.0
205
+ narwhals==1.25.2
206
+ nbclient @ file:///home/conda/feedstock_root/build_artifacts/nbclient_1734628800805/work
207
+ nbconvert @ file:///home/conda/feedstock_root/build_artifacts/nbconvert-meta_1736258671456/work
208
+ nbformat @ file:///home/conda/feedstock_root/build_artifacts/nbformat_1733402752141/work
209
+ nest_asyncio @ file:///home/conda/feedstock_root/build_artifacts/nest-asyncio_1733325553580/work
210
+ networkx==2.8.8
211
+ nltk==3.9.1
212
+ notebook_shim @ file:///home/conda/feedstock_root/build_artifacts/notebook-shim_1733408315203/work
213
+ num2words==0.5.14
214
+ numba==0.61.0
215
+ numexpr==2.10.2
216
+ numpy @ file:///Users/runner/miniforge3/conda-bld/numpy_1707225640867/work/dist/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl#sha256=b62b8b824024855c3eb209bd45bfcf60bc9a11b94cbf9d5b7b25dc503021e672
217
+ oauthlib @ file:///home/conda/feedstock_root/build_artifacts/oauthlib_1733752848439/work
218
+ ollama==0.4.7
219
+ onnxruntime @ file:///Users/runner/miniforge3/conda-bld/onnxruntime_1735443977499/work/build-ci/Release/dist/onnxruntime-1.20.1-cp311-cp311-macosx_11_0_arm64.whl#sha256=f2a0666c1ab7a3326e5ff6b4c81cef4246c1d88692eff576fb61d17628379742
220
+ openai==1.68.2
221
+ openai-whisper==20240930
222
+ openapi-schema-pydantic==1.2.4
223
+ opentelemetry-api==1.29.0
224
+ opentelemetry-exporter-otlp-proto-common==1.29.0
225
+ opentelemetry-exporter-otlp-proto-grpc==1.29.0
226
+ opentelemetry-instrumentation==0.50b0
227
+ opentelemetry-instrumentation-asgi==0.50b0
228
+ opentelemetry-instrumentation-fastapi==0.50b0
229
+ opentelemetry-proto==1.29.0
230
+ opentelemetry-sdk==1.29.0
231
+ opentelemetry-semantic-conventions==0.50b0
232
+ opentelemetry-util-http==0.50b0
233
+ orjson==3.10.14
234
+ outcome==1.3.0.post0
235
+ overrides @ file:///home/conda/feedstock_root/build_artifacts/overrides_1734587627321/work
236
+ packaging==23.2
237
+ pandas==1.5.3
238
+ pandocfilters @ file:///home/conda/feedstock_root/build_artifacts/pandocfilters_1631603243851/work
239
+ parso @ file:///home/conda/feedstock_root/build_artifacts/parso_1733271261340/work
240
+ pathtools==0.1.2
241
+ peft==0.4.0
242
+ pexpect @ file:///home/conda/feedstock_root/build_artifacts/pexpect_1733301927746/work
243
+ pickleshare @ file:///home/conda/feedstock_root/build_artifacts/pickleshare_1733327343728/work
244
+ pillow @ file:///Users/runner/miniforge3/conda-bld/pillow_1735929745664/work
245
+ pkgutil_resolve_name @ file:///home/conda/feedstock_root/build_artifacts/pkgutil-resolve-name_1733344503739/work
246
+ platformdirs @ file:///home/conda/feedstock_root/build_artifacts/platformdirs_1733232627818/work
247
+ plotly==5.15.0
248
+ pooch==1.8.2
249
+ posthog @ file:///home/conda/feedstock_root/build_artifacts/posthog_1726036274425/work
250
+ preshed==3.0.9
251
+ prometheus_client @ file:///home/conda/feedstock_root/build_artifacts/prometheus_client_1733327310477/work
252
+ prompt_toolkit @ file:///home/conda/feedstock_root/build_artifacts/prompt-toolkit_1733302527033/work
253
+ propcache @ file:///Users/runner/miniforge3/conda-bld/propcache_1737635547864/work
254
+ proto-plus==1.25.0
255
+ protobuf==5.29.4
256
+ psutil==5.9.8
257
+ ptyprocess @ file:///home/conda/feedstock_root/build_artifacts/ptyprocess_1733302279685/work/dist/ptyprocess-0.7.0-py2.py3-none-any.whl#sha256=92c32ff62b5fd8cf325bec5ab90d7be3d2a8ca8c8a3813ff487a8d2002630d1f
258
+ pulsar-client @ file:///Users/runner/miniforge3/conda-bld/pulsar-client_1725542299588/work/dist/pulsar_client-3.5.0-cp311-cp311-macosx_11_0_arm64.whl#sha256=1570a636e1ae2647174be973a3c55b8f54e046f2e549b4e0043a4e18c9090df2
259
+ pure_eval @ file:///home/conda/feedstock_root/build_artifacts/pure_eval_1733569405015/work
260
+ pyarrow==17.0.0
261
+ pyasn1 @ file:///home/conda/feedstock_root/build_artifacts/pyasn1_1733217608156/work
262
+ pyasn1_modules @ file:///home/conda/feedstock_root/build_artifacts/pyasn1-modules_1733324602540/work
263
+ pycparser @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_pycparser_1733195786/work
264
+ pydantic==2.10.6
265
+ pydantic-settings==2.7.1
266
+ pydantic_core @ file:///Users/runner/miniforge3/conda-bld/pydantic-core_1734571525262/work
267
+ pydub @ file:///home/conda/feedstock_root/build_artifacts/pydub_1734638032476/work
268
+ pygame==2.6.1
269
+ Pygments @ file:///home/conda/feedstock_root/build_artifacts/pygments_1736243443484/work
270
+ PyJWT @ file:///home/conda/feedstock_root/build_artifacts/pyjwt_1732782409051/work
271
+ pynndescent==0.5.13
272
+ pyobjc==11.0
273
+ pyobjc-core==11.0
274
+ pyobjc-framework-Accessibility==11.0
275
+ pyobjc-framework-Accounts==11.0
276
+ pyobjc-framework-AddressBook==11.0
277
+ pyobjc-framework-AdServices==11.0
278
+ pyobjc-framework-AdSupport==11.0
279
+ pyobjc-framework-AppleScriptKit==11.0
280
+ pyobjc-framework-AppleScriptObjC==11.0
281
+ pyobjc-framework-ApplicationServices==11.0
282
+ pyobjc-framework-AppTrackingTransparency==11.0
283
+ pyobjc-framework-AudioVideoBridging==11.0
284
+ pyobjc-framework-AuthenticationServices==11.0
285
+ pyobjc-framework-AutomaticAssessmentConfiguration==11.0
286
+ pyobjc-framework-Automator==11.0
287
+ pyobjc-framework-AVFoundation==11.0
288
+ pyobjc-framework-AVKit==11.0
289
+ pyobjc-framework-AVRouting==11.0
290
+ pyobjc-framework-BackgroundAssets==11.0
291
+ pyobjc-framework-BrowserEngineKit==11.0
292
+ pyobjc-framework-BusinessChat==11.0
293
+ pyobjc-framework-CalendarStore==11.0
294
+ pyobjc-framework-CallKit==11.0
295
+ pyobjc-framework-Carbon==11.0
296
+ pyobjc-framework-CFNetwork==11.0
297
+ pyobjc-framework-Cinematic==11.0
298
+ pyobjc-framework-ClassKit==11.0
299
+ pyobjc-framework-CloudKit==11.0
300
+ pyobjc-framework-Cocoa==11.0
301
+ pyobjc-framework-Collaboration==11.0
302
+ pyobjc-framework-ColorSync==11.0
303
+ pyobjc-framework-Contacts==11.0
304
+ pyobjc-framework-ContactsUI==11.0
305
+ pyobjc-framework-CoreAudio==11.0
306
+ pyobjc-framework-CoreAudioKit==11.0
307
+ pyobjc-framework-CoreBluetooth==11.0
308
+ pyobjc-framework-CoreData==11.0
309
+ pyobjc-framework-CoreHaptics==11.0
310
+ pyobjc-framework-CoreLocation==11.0
311
+ pyobjc-framework-CoreMedia==11.0
312
+ pyobjc-framework-CoreMediaIO==11.0
313
+ pyobjc-framework-CoreMIDI==11.0
314
+ pyobjc-framework-CoreML==11.0
315
+ pyobjc-framework-CoreMotion==11.0
316
+ pyobjc-framework-CoreServices==11.0
317
+ pyobjc-framework-CoreSpotlight==11.0
318
+ pyobjc-framework-CoreText==11.0
319
+ pyobjc-framework-CoreWLAN==11.0
320
+ pyobjc-framework-CryptoTokenKit==11.0
321
+ pyobjc-framework-DataDetection==11.0
322
+ pyobjc-framework-DeviceCheck==11.0
323
+ pyobjc-framework-DeviceDiscoveryExtension==11.0
324
+ pyobjc-framework-DictionaryServices==11.0
325
+ pyobjc-framework-DiscRecording==11.0
326
+ pyobjc-framework-DiscRecordingUI==11.0
327
+ pyobjc-framework-DiskArbitration==11.0
328
+ pyobjc-framework-DVDPlayback==11.0
329
+ pyobjc-framework-EventKit==11.0
330
+ pyobjc-framework-ExceptionHandling==11.0
331
+ pyobjc-framework-ExecutionPolicy==11.0
332
+ pyobjc-framework-ExtensionKit==11.0
333
+ pyobjc-framework-ExternalAccessory==11.0
334
+ pyobjc-framework-FileProvider==11.0
335
+ pyobjc-framework-FileProviderUI==11.0
336
+ pyobjc-framework-FinderSync==11.0
337
+ pyobjc-framework-FSEvents==11.0
338
+ pyobjc-framework-GameCenter==11.0
339
+ pyobjc-framework-GameController==11.0
340
+ pyobjc-framework-GameKit==11.0
341
+ pyobjc-framework-GameplayKit==11.0
342
+ pyobjc-framework-HealthKit==11.0
343
+ pyobjc-framework-ImageCaptureCore==11.0
344
+ pyobjc-framework-InputMethodKit==11.0
345
+ pyobjc-framework-InstallerPlugins==11.0
346
+ pyobjc-framework-InstantMessage==11.0
347
+ pyobjc-framework-Intents==11.0
348
+ pyobjc-framework-IntentsUI==11.0
349
+ pyobjc-framework-IOBluetooth==11.0
350
+ pyobjc-framework-IOBluetoothUI==11.0
351
+ pyobjc-framework-IOSurface==11.0
352
+ pyobjc-framework-iTunesLibrary==11.0
353
+ pyobjc-framework-KernelManagement==11.0
354
+ pyobjc-framework-LatentSemanticMapping==11.0
355
+ pyobjc-framework-LaunchServices==11.0
356
+ pyobjc-framework-libdispatch==11.0
357
+ pyobjc-framework-libxpc==11.0
358
+ pyobjc-framework-LinkPresentation==11.0
359
+ pyobjc-framework-LocalAuthentication==11.0
360
+ pyobjc-framework-LocalAuthenticationEmbeddedUI==11.0
361
+ pyobjc-framework-MailKit==11.0
362
+ pyobjc-framework-MapKit==11.0
363
+ pyobjc-framework-MediaAccessibility==11.0
364
+ pyobjc-framework-MediaExtension==11.0
365
+ pyobjc-framework-MediaLibrary==11.0
366
+ pyobjc-framework-MediaPlayer==11.0
367
+ pyobjc-framework-MediaToolbox==11.0
368
+ pyobjc-framework-Metal==11.0
369
+ pyobjc-framework-MetalFX==11.0
370
+ pyobjc-framework-MetalKit==11.0
371
+ pyobjc-framework-MetalPerformanceShaders==11.0
372
+ pyobjc-framework-MetalPerformanceShadersGraph==11.0
373
+ pyobjc-framework-MetricKit==11.0
374
+ pyobjc-framework-MLCompute==11.0
375
+ pyobjc-framework-ModelIO==11.0
376
+ pyobjc-framework-MultipeerConnectivity==11.0
377
+ pyobjc-framework-NaturalLanguage==11.0
378
+ pyobjc-framework-NetFS==11.0
379
+ pyobjc-framework-Network==11.0
380
+ pyobjc-framework-NetworkExtension==11.0
381
+ pyobjc-framework-NotificationCenter==11.0
382
+ pyobjc-framework-OpenDirectory==11.0
383
+ pyobjc-framework-OSAKit==11.0
384
+ pyobjc-framework-OSLog==11.0
385
+ pyobjc-framework-PassKit==11.0
386
+ pyobjc-framework-PencilKit==11.0
387
+ pyobjc-framework-PHASE==11.0
388
+ pyobjc-framework-Photos==11.0
389
+ pyobjc-framework-PhotosUI==11.0
390
+ pyobjc-framework-PreferencePanes==11.0
391
+ pyobjc-framework-PushKit==11.0
392
+ pyobjc-framework-Quartz==11.0
393
+ pyobjc-framework-QuickLookThumbnailing==11.0
394
+ pyobjc-framework-ReplayKit==11.0
395
+ pyobjc-framework-SafariServices==11.0
396
+ pyobjc-framework-SafetyKit==11.0
397
+ pyobjc-framework-SceneKit==11.0
398
+ pyobjc-framework-ScreenCaptureKit==11.0
399
+ pyobjc-framework-ScreenSaver==11.0
400
+ pyobjc-framework-ScreenTime==11.0
401
+ pyobjc-framework-ScriptingBridge==11.0
402
+ pyobjc-framework-SearchKit==11.0
403
+ pyobjc-framework-Security==11.0
404
+ pyobjc-framework-SecurityFoundation==11.0
405
+ pyobjc-framework-SecurityInterface==11.0
406
+ pyobjc-framework-SensitiveContentAnalysis==11.0
407
+ pyobjc-framework-ServiceManagement==11.0
408
+ pyobjc-framework-SharedWithYou==11.0
409
+ pyobjc-framework-SharedWithYouCore==11.0
410
+ pyobjc-framework-ShazamKit==11.0
411
+ pyobjc-framework-Social==11.0
412
+ pyobjc-framework-SoundAnalysis==11.0
413
+ pyobjc-framework-Speech==11.0
414
+ pyobjc-framework-SpriteKit==11.0
415
+ pyobjc-framework-StoreKit==11.0
416
+ pyobjc-framework-Symbols==11.0
417
+ pyobjc-framework-SyncServices==11.0
418
+ pyobjc-framework-SystemConfiguration==11.0
419
+ pyobjc-framework-SystemExtensions==11.0
420
+ pyobjc-framework-ThreadNetwork==11.0
421
+ pyobjc-framework-UniformTypeIdentifiers==11.0
422
+ pyobjc-framework-UserNotifications==11.0
423
+ pyobjc-framework-UserNotificationsUI==11.0
424
+ pyobjc-framework-VideoSubscriberAccount==11.0
425
+ pyobjc-framework-VideoToolbox==11.0
426
+ pyobjc-framework-Virtualization==11.0
427
+ pyobjc-framework-Vision==11.0
428
+ pyobjc-framework-WebKit==11.0
429
+ pyOpenSSL @ file:///home/conda/feedstock_root/build_artifacts/pyopenssl_1737243356468/work
430
+ pyparsing==3.0.9
431
+ PyPDF2==3.0.1
432
+ PyPika @ file:///home/conda/feedstock_root/build_artifacts/pypika_1734974148700/work
433
+ pypinyin==0.53.0
434
+ pyproject_hooks @ file:///home/conda/feedstock_root/build_artifacts/pyproject_hooks_1733710025763/work
435
+ pysbd==0.3.4
436
+ PySocks @ file:///home/conda/feedstock_root/build_artifacts/pysocks_1733217236728/work
437
+ pytesseract==0.3.13
438
+ python-crfsuite==0.9.11
439
+ python-dateutil @ file:///home/conda/feedstock_root/build_artifacts/python-dateutil_1733215673016/work
440
+ python-dotenv @ file:///home/conda/feedstock_root/build_artifacts/python-dotenv_1733243180666/work
441
+ python-json-logger @ file:///home/conda/feedstock_root/build_artifacts/python-json-logger_1677079630776/work
442
+ python-multipart @ file:///home/conda/feedstock_root/build_artifacts/python-multipart_1734420773152/work
443
+ pyttsx3==2.98
444
+ pytz @ file:///home/conda/feedstock_root/build_artifacts/pytz_1706886791323/work
445
+ pyu2f @ file:///home/conda/feedstock_root/build_artifacts/pyu2f_1733738580568/work
446
+ PyYAML @ file:///Users/runner/miniforge3/conda-bld/pyyaml_1725456256847/work
447
+ pyzmq @ file:///Users/runner/miniforge3/conda-bld/pyzmq_1728642247211/work
448
+ RapidFuzz==3.12.2
449
+ referencing @ file:///home/conda/feedstock_root/build_artifacts/referencing_1733366743674/work
450
+ regex==2024.11.6
451
+ requests==2.28.2
452
+ requests-oauthlib @ file:///home/conda/feedstock_root/build_artifacts/requests-oauthlib_1733772243268/work
453
+ requests-toolbelt==1.0.0
454
+ retrying @ file:///home/conda/feedstock_root/build_artifacts/retrying_1721646300555/work
455
+ rfc3339_validator @ file:///home/conda/feedstock_root/build_artifacts/rfc3339-validator_1733599910982/work
456
+ rfc3986-validator @ file:///home/conda/feedstock_root/build_artifacts/rfc3986-validator_1598024191506/work
457
+ rich @ file:///home/conda/feedstock_root/build_artifacts/rich_1733342254348/work/dist
458
+ rich-toolkit @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_rich-toolkit_1733750834/work
459
+ rpds-py @ file:///Users/runner/miniforge3/conda-bld/rpds-py_1733366661843/work
460
+ rsa @ file:///home/conda/feedstock_root/build_artifacts/rsa_1733662684165/work
461
+ ruff==0.9.5
462
+ safehttpx @ file:///home/conda/feedstock_root/build_artifacts/safehttpx_1734360621373/work
463
+ safetensors==0.5.2
464
+ scikit-learn==1.3.0
465
+ scipy==1.13.1
466
+ selenium==4.10.0
467
+ semantic-version @ file:///home/conda/feedstock_root/build_artifacts/semantic_version_1653579368137/work
468
+ Send2Trash @ file:///Users/runner/miniforge3/conda-bld/send2trash_1733322099476/work
469
+ sentence-transformers==2.2.2
470
+ sentencepiece @ file:///Users/runner/miniforge3/conda-bld/sentencepiece-split_1727549050457/work/python
471
+ sentry-sdk==2.20.0
472
+ setproctitle==1.3.4
473
+ sgmllib3k==1.0.0
474
+ shellingham @ file:///home/conda/feedstock_root/build_artifacts/shellingham_1733300899265/work
475
+ sigtools==4.0.1
476
+ simpleaudio==1.0.4
477
+ six @ file:///home/conda/feedstock_root/build_artifacts/six_1733380938961/work
478
+ smart-open==7.1.0
479
+ smmap==5.0.2
480
+ sniffio @ file:///home/conda/feedstock_root/build_artifacts/sniffio_1733244044561/work
481
+ sortedcontainers==2.4.0
482
+ sounddevice==0.5.1
483
+ soundfile==0.13.1
484
+ soupsieve @ file:///home/conda/feedstock_root/build_artifacts/soupsieve_1693929250441/work
485
+ soxr==0.5.0.post1
486
+ spaces==0.32.0
487
+ spacy==3.8.4
488
+ spacy-legacy==3.0.12
489
+ spacy-loggers==1.0.5
490
+ SpeechRecognition==3.14.1
491
+ speedtest-cli==2.1.3
492
+ SQLAlchemy==1.4.54
493
+ srsly==2.5.1
494
+ stack_data @ file:///home/conda/feedstock_root/build_artifacts/stack_data_1733569443808/work
495
+ starlette @ file:///home/conda/feedstock_root/build_artifacts/starlette_1733344384719/work
496
+ SudachiDict-core==20250129
497
+ SudachiPy==0.6.10
498
+ sympy==1.13.1
499
+ synchronicity==0.9.10
500
+ tenacity==8.5.0
501
+ tensorboard==2.19.0
502
+ tensorboard-data-server==0.7.2
503
+ terminado @ file:///Users/runner/miniforge3/conda-bld/terminado_1710263781917/work
504
+ thinc==8.3.4
505
+ threadpoolctl @ file:///home/conda/feedstock_root/build_artifacts/threadpoolctl_1714400101435/work
506
+ tiktoken==0.8.0
507
+ tinycss2 @ file:///home/conda/feedstock_root/build_artifacts/tinycss2_1729802851396/work
508
+ tokenizers==0.13.3
509
+ toml==0.10.2
510
+ tomli @ file:///home/conda/feedstock_root/build_artifacts/tomli_1733256695513/work
511
+ tomlkit @ file:///home/conda/feedstock_root/build_artifacts/tomlkit_1733230743009/work
512
+ torch==2.6.0
513
+ torchaudio==2.6.0
514
+ torchvision==0.21.0
515
+ tornado @ file:///Users/runner/miniforge3/conda-bld/tornado_1732615936501/work
516
+ tqdm @ file:///home/conda/feedstock_root/build_artifacts/tqdm_1735661334605/work
517
+ trainer==0.0.36
518
+ traitlets @ file:///home/conda/feedstock_root/build_artifacts/traitlets_1733367359838/work
519
+ transformers==4.30.0
520
+ trio==0.29.0
521
+ trio-websocket==0.12.2
522
+ trl==0.5.0
523
+ TTS==0.22.0
524
+ twilio==9.4.4
525
+ typeguard==4.4.2
526
+ typer==0.15.1
527
+ typer-slim==0.15.1
528
+ types-certifi==2021.10.8.3
529
+ types-python-dateutil @ file:///home/conda/feedstock_root/build_artifacts/types-python-dateutil_1733612335562/work
530
+ types-toml==0.10.8.20240310
531
+ typing-inspect==0.9.0
532
+ typing_extensions @ file:///home/conda/feedstock_root/build_artifacts/typing_extensions_1733188668063/work
533
+ typing_utils @ file:///home/conda/feedstock_root/build_artifacts/typing_utils_1733331286120/work
534
+ tzdata @ file:///home/conda/feedstock_root/build_artifacts/python-tzdata_1733235305708/work
535
+ tzlocal==5.3.1
536
+ uc-micro-py==1.0.3
537
+ umap==0.1.1
538
+ umap-learn==0.5.7
539
+ unicodedata2 @ file:///Users/runner/miniforge3/conda-bld/unicodedata2_1729704585212/work
540
+ Unidecode==1.3.8
541
+ uri-template @ file:///home/conda/feedstock_root/build_artifacts/uri-template_1733323593477/work/dist
542
+ uritemplate==4.1.1
543
+ urllib3==1.26.20
544
+ uvicorn @ file:///home/conda/feedstock_root/build_artifacts/uvicorn_1734292939144/work
545
+ uvloop @ file:///Users/runner/miniforge3/conda-bld/uvloop_1730214404471/work
546
+ wandb==0.15.8
547
+ wasabi==1.1.3
548
+ watchfiles @ file:///Users/runner/miniforge3/conda-bld/watchfiles_1736550451638/work
549
+ wcwidth @ file:///home/conda/feedstock_root/build_artifacts/wcwidth_1733231326287/work
550
+ weasel==0.4.1
551
+ webcolors @ file:///home/conda/feedstock_root/build_artifacts/webcolors_1733359735138/work
552
+ webdriver-manager==3.8.6
553
+ webencodings @ file:///home/conda/feedstock_root/build_artifacts/webencodings_1733236011802/work
554
+ websocket-client @ file:///home/conda/feedstock_root/build_artifacts/websocket-client_1733157342724/work
555
+ websockets==11.0.3
556
+ Werkzeug==2.2.3
557
+ whisper==1.1.10
558
+ widgetsnbextension @ file:///home/conda/feedstock_root/build_artifacts/widgetsnbextension_1733128559935/work
559
+ wrapt==1.17.1
560
+ wsproto==1.2.0
561
+ xxhash==3.5.0
562
+ yarl @ file:///Users/runner/miniforge3/conda-bld/yarl_1737575825523/work
563
+ zipp @ file:///home/conda/feedstock_root/build_artifacts/zipp_1732827521216/work
564
+ zstandard==0.23.0
requirements2.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=3.41.2
2
+ openai>=0.28.0
3
+ anthropic>=0.5.0
4
+ python-dotenv>=1.0.0
5
+ langchain>=0.0.267
6
+ faiss-cpu>=1.7.4
7
+ SpeechRecognition>=3.10.0
8
+ gtts>=2.3.2
9
+ playsound>=1.3.0
10
+ PyAudio>=0.2.13
services/__init__.py ADDED
File without changes
services/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (260 Bytes). View file
 
services/__pycache__/auth_service.cpython-311.pyc ADDED
Binary file (5.87 kB). View file
 
services/__pycache__/chatbot_rag.cpython-311.pyc ADDED
Binary file (5.59 kB). View file
 
services/__pycache__/data_service.cpython-311.pyc ADDED
Binary file (1.19 kB). View file
 
services/__pycache__/file_service.cpython-311.pyc ADDED
Binary file (9.18 kB). View file
 
services/__pycache__/general_chat_service.cpython-311.pyc ADDED
Binary file (2.58 kB). View file
 
services/__pycache__/response_service.cpython-311.pyc ADDED
Binary file (763 Bytes). View file
 
services/__pycache__/study_support_service.cpython-311.pyc ADDED
Binary file (1.52 kB). View file
 
services/__pycache__/system_prompts_service.cpython-311.pyc ADDED
Binary file (1.14 kB). View file
 
services/__pycache__/ui_service.cpython-311.pyc ADDED
Binary file (57.8 kB). View file
 
services/__pycache__/voice_interface.cpython-311.pyc ADDED
Binary file (8.18 kB). View file
 
services/auth_service.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # services/auth_service.py
2
+
3
+ import time
4
+ import threading
5
+ import uuid
6
+ from typing import Dict, Optional, Any, Tuple
7
+
8
+ # A simple dictionary of sample credentials.
9
+ users = {
10
+ "adabor@usiu.ac.ke": "student123",
11
+ "adavid@usiu.ac.ke": "faculty123",
12
+ "jmilton@usiu.ac.ke": "staff123"
13
+ }
14
+
15
+ # Active user sessions storage
16
+ active_sessions: Dict[str, Dict[str, Any]] = {}
17
+
18
+ # Inactivity timeout in seconds (15 minutes)
19
+ INACTIVITY_TIMEOUT = 15 * 60
20
+
21
+ def authenticate(user_id: str, password: str) -> bool:
22
+ """Return True if the provided credentials match the lookup table."""
23
+ return users.get(user_id) == password
24
+
25
+ def create_session(user_id: str) -> str:
26
+ """
27
+ Create a new user session with a unique session ID
28
+ Returns the session ID
29
+ """
30
+ session_id = str(uuid.uuid4())
31
+ active_sessions[session_id] = {
32
+ "user_id": user_id,
33
+ "logged_in": True,
34
+ "last_activity": time.time(),
35
+ "inactivity_timer": None,
36
+ "warned": False,
37
+ "created_at": time.time()
38
+ }
39
+
40
+ # Set the initial inactivity timer
41
+ timer = threading.Timer(INACTIVITY_TIMEOUT, check_inactivity, args=[session_id])
42
+ timer.daemon = True
43
+ timer.start()
44
+ active_sessions[session_id]["inactivity_timer"] = timer
45
+
46
+ return session_id
47
+
48
+ def get_session(session_id: str) -> Optional[Dict[str, Any]]:
49
+ """Get session data for a given session ID"""
50
+ return active_sessions.get(session_id)
51
+
52
+ def update_session_activity(session_id: str) -> bool:
53
+ """
54
+ Update the last activity timestamp for a session
55
+ Returns True if session was found and updated, False otherwise
56
+ """
57
+ if session_id in active_sessions:
58
+ active_sessions[session_id]["last_activity"] = time.time()
59
+ active_sessions[session_id]["warned"] = False
60
+
61
+ # Reset inactivity timer if it exists
62
+ if active_sessions[session_id]["inactivity_timer"]:
63
+ active_sessions[session_id]["inactivity_timer"].cancel()
64
+
65
+ # Set new inactivity timer
66
+ timer = threading.Timer(INACTIVITY_TIMEOUT, check_inactivity, args=[session_id])
67
+ timer.daemon = True
68
+ timer.start()
69
+ active_sessions[session_id]["inactivity_timer"] = timer
70
+ return True
71
+ return False
72
+
73
+ def end_session(session_id: str) -> bool:
74
+ """
75
+ End a user session
76
+ Returns True if session was found and ended, False otherwise
77
+ """
78
+ if session_id in active_sessions:
79
+ # Cancel the inactivity timer if it exists
80
+ if active_sessions[session_id]["inactivity_timer"]:
81
+ active_sessions[session_id]["inactivity_timer"].cancel()
82
+
83
+ # Remove the session
84
+ del active_sessions[session_id]
85
+ return True
86
+ return False
87
+
88
+ def check_inactivity(session_id: str) -> None:
89
+ """Check if the session has been inactive for too long and mark it as logged out if so"""
90
+ if session_id in active_sessions:
91
+ current_time = time.time()
92
+ last_activity = active_sessions[session_id]["last_activity"]
93
+
94
+ if current_time - last_activity >= INACTIVITY_TIMEOUT:
95
+ active_sessions[session_id]["logged_in"] = False
96
+ # The actual logout action will be handled by the UI when it detects this change
97
+
98
+ def get_active_sessions() -> Dict[str, Dict[str, Any]]:
99
+ """Get all active sessions (for admin or debugging purposes)"""
100
+ return active_sessions
101
+
102
+ def clear_expired_sessions() -> int:
103
+ """
104
+ Clear all expired sessions
105
+ Returns the number of sessions cleared
106
+ """
107
+ expired_count = 0
108
+ sessions_to_remove = []
109
+
110
+ for session_id, session_data in active_sessions.items():
111
+ if not session_data["logged_in"]:
112
+ sessions_to_remove.append(session_id)
113
+
114
+ for session_id in sessions_to_remove:
115
+ end_session(session_id)
116
+ expired_count += 1
117
+
118
+ return expired_count
119
+
120
+ # Start a background thread to periodically clean up expired sessions
121
+ def cleanup_sessions_periodically(interval=3600): # Default: every hour
122
+ """Start a background thread to periodically clean up expired sessions"""
123
+ def cleanup_task():
124
+ while True:
125
+ clear_expired_sessions()
126
+ time.sleep(interval)
127
+
128
+ cleanup_thread = threading.Thread(target=cleanup_task, daemon=True)
129
+ cleanup_thread.start()
130
+
131
+ # Initialize the session cleanup thread
132
+ cleanup_sessions_periodically()
services/data_service-old1.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # services/data_service.py
2
+ from services.system_prompts_service import SystemPromptsService
3
+ from services.response_service import ResponseService
4
+ from configs.config import GPT4O_MODEL, CLAUDE_MODEL, GPT4O_API_KEY, CLAUDE_API_KEY
5
+ from services.general_chat_service import GeneralChatService
6
+ from services.study_support_service import StudySupportService
7
+ from langchain.embeddings import OpenAIEmbeddings
8
+ from langchain.vectorstores import Chroma
9
+
10
+ class DataService:
11
+ """
12
+ Data service that routes user queries to the appropriate agent:
13
+ - General academic inquiries use GPT-4o via RAG if a retriever is provided,
14
+ using the general_chat function from GeneralChatService.
15
+ - Study support inquiries use Claude 3.5 Sonnet.
16
+ """
17
+ def __init__(self, retriever=None) -> None:
18
+ self.prompts_service = SystemPromptsService()
19
+ self.response_service = ResponseService()
20
+
21
+ # If a retriever is provided as a string (e.g., a directory path),
22
+ # convert it into a proper retriever object.
23
+ if retriever and isinstance(retriever, str):
24
+ embeddings = OpenAIEmbeddings(openai_api_key=GPT4O_API_KEY)
25
+ vectorstore = Chroma(persist_directory=retriever, embedding_function=embeddings)
26
+ retriever = vectorstore.as_retriever(
27
+ search_type="mmr",
28
+ search_kwargs={'k': 40, 'lambda_mult': 0.6}
29
+ )
30
+
31
+ self.general_chat_service = GeneralChatService(
32
+ model=GPT4O_MODEL,
33
+ api_key=GPT4O_API_KEY,
34
+ system_prompt=self.prompts_service.get_prompt()[0],
35
+ retriever=retriever # RAG will be used if this is a valid retriever object.
36
+ )
37
+ self.study_support_service = StudySupportService(
38
+ model=CLAUDE_MODEL,
39
+ api_key=CLAUDE_API_KEY,
40
+ system_prompt=self.prompts_service.get_prompt()[1]
41
+ )
42
+
43
+ def route_query(self, query: str, chat_history: any, query_type: str = "general") -> str:
44
+ """
45
+ Routes the user query to the appropriate service based on query type.
46
+ - If query_type is "study", the study support service is used.
47
+ - Otherwise, the general chat service (using general_chat) is used.
48
+ The final response is formatted via the response_service.
49
+ """
50
+ if query_type == "study":
51
+ response = self.study_support_service.ask(query)
52
+ else:
53
+ # Call the general_chat function which streams responses.
54
+ # Convert the generator into a list, and use the final yielded value.
55
+ responses = list(self.general_chat_service.general_chat(query, chat_history))
56
+ response = responses[-1] if responses else ""
57
+ return self.response_service.format_response(response)
services/data_service.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # services/data_service.py
2
+ from services.system_prompts_service import SystemPromptsService
3
+
4
+ class DataService:
5
+ """
6
+ Data service that provides access to system prompts and manages data-related operations.
7
+ """
8
+ def __init__(self, retriever=None) -> None:
9
+ self.prompts_service = SystemPromptsService()
10
+ self.retriever = retriever
11
+
12
+ def get_retriever(self):
13
+ """Get the configured retriever for RAG"""
14
+ return self.retriever
services/file_service.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # services/file_service.py
2
+
3
+ import os
4
+ import io
5
+ import PyPDF2
6
+ import docx
7
+ import pandas as pd
8
+ import pptx
9
+ from PIL import Image
10
+ import openai
11
+ import csv
12
+ from typing import Dict, Any
13
+
14
+ class FileProcessor:
15
+ """Service for processing different types of files"""
16
+
17
+ @staticmethod
18
+ def read_file(file) -> str:
19
+ """
20
+ Extract content from various file types
21
+ Returns the content as a string
22
+ """
23
+ if file is None:
24
+ return "No file provided"
25
+
26
+ file_extension = os.path.splitext(file.name)[1].lower()
27
+
28
+ if file_extension == '.txt':
29
+ content = file.read().decode('utf-8')
30
+ elif file_extension == '.pdf':
31
+ pdf_reader = PyPDF2.PdfReader(file)
32
+ content = ''
33
+ for page in pdf_reader.pages:
34
+ text = page.extract_text()
35
+ if text:
36
+ content += text
37
+ elif file_extension in ['.docx', '.doc']:
38
+ doc = docx.Document(file)
39
+ content = "\n".join([para.text for para in doc.paragraphs])
40
+ elif file_extension in ['.xlsx', '.xls']:
41
+ df = pd.read_excel(file)
42
+ content = df.to_string()
43
+ elif file_extension == '.csv':
44
+ # Reset file pointer to the beginning
45
+ file.seek(0)
46
+
47
+ # First, try to read with pandas
48
+ try:
49
+ df = pd.read_csv(file)
50
+ content = df.to_string()
51
+ except Exception as e:
52
+ # If pandas fails, try with csv module
53
+ file.seek(0)
54
+ try:
55
+ csv_content = []
56
+ csv_reader = csv.reader(io.StringIO(file.read().decode('utf-8')))
57
+ for row in csv_reader:
58
+ csv_content.append(','.join(row))
59
+ content = '\n'.join(csv_content)
60
+ except Exception as csv_e:
61
+ content = f"Error processing CSV file: {str(csv_e)}"
62
+ elif file_extension in ['.pptx', '.ppt']:
63
+ prs = pptx.Presentation(file)
64
+ content = ''
65
+ for slide in prs.slides:
66
+ for shape in slide.shapes:
67
+ if hasattr(shape, 'text'):
68
+ content += shape.text + '\n'
69
+ elif file_extension in ['.png', '.jpg', '.jpeg', '.gif']:
70
+ try:
71
+ image = Image.open(file)
72
+ buffered = io.BytesIO()
73
+ image.save(buffered, format="PNG")
74
+ # Create an image variation via OpenAI's API
75
+ response = openai.Image.create_variation(
76
+ image=buffered,
77
+ n=1,
78
+ size="256x256"
79
+ )
80
+ image_description = response['data'][0]['url']
81
+ content = f"[Image uploaded. Description: {image_description}]"
82
+ except Exception as e:
83
+ content = f"Error processing image: {str(e)}"
84
+ else:
85
+ content = f"Unsupported file type: {file_extension}"
86
+
87
+ return content
88
+
89
+ @staticmethod
90
+ def get_file_metadata(file) -> Dict[str, Any]:
91
+ """
92
+ Get metadata about a file
93
+ Returns a dictionary of file metadata
94
+ """
95
+ if file is None:
96
+ return {"error": "No file provided"}
97
+
98
+ try:
99
+ file_extension = os.path.splitext(file.name)[1].lower()
100
+ file_size = os.path.getsize(file.name) if hasattr(file, 'name') else 0
101
+
102
+ metadata = {
103
+ "filename": os.path.basename(file.name),
104
+ "extension": file_extension,
105
+ "size_bytes": file_size,
106
+ "size_formatted": FileProcessor._format_file_size(file_size)
107
+ }
108
+
109
+ # Get additional metadata based on file type
110
+ if file_extension == '.pdf':
111
+ pdf_reader = PyPDF2.PdfReader(file)
112
+ metadata["pages"] = len(pdf_reader.pages)
113
+ if pdf_reader.metadata:
114
+ metadata["author"] = pdf_reader.metadata.author
115
+ metadata["creator"] = pdf_reader.metadata.creator
116
+ metadata["producer"] = pdf_reader.metadata.producer
117
+ elif file_extension in ['.docx', '.doc']:
118
+ doc = docx.Document(file)
119
+ metadata["paragraphs"] = len(doc.paragraphs)
120
+ elif file_extension in ['.xlsx', '.xls']:
121
+ df = pd.read_excel(file)
122
+ metadata["rows"] = len(df)
123
+ metadata["columns"] = len(df.columns)
124
+ elif file_extension == '.csv':
125
+ # Reset file pointer
126
+ file.seek(0)
127
+ try:
128
+ df = pd.read_csv(file)
129
+ metadata["rows"] = len(df)
130
+ metadata["columns"] = len(df.columns)
131
+
132
+ # Get sample of column names
133
+ col_sample = df.columns.tolist()[:5]
134
+ if len(df.columns) > 5:
135
+ col_sample.append("...")
136
+ metadata["column_sample"] = col_sample
137
+ except Exception:
138
+ # If pandas fails, try with csv module
139
+ file.seek(0)
140
+ try:
141
+ csv_reader = csv.reader(io.StringIO(file.read().decode('utf-8')))
142
+ rows = list(csv_reader)
143
+ metadata["rows"] = len(rows)
144
+ if rows:
145
+ metadata["columns"] = len(rows[0])
146
+ except Exception:
147
+ pass
148
+ elif file_extension in ['.pptx', '.ppt']:
149
+ prs = pptx.Presentation(file)
150
+ metadata["slides"] = len(prs.slides)
151
+ elif file_extension in ['.png', '.jpg', '.jpeg', '.gif']:
152
+ image = Image.open(file)
153
+ metadata["dimensions"] = f"{image.width}x{image.height}"
154
+ metadata["mode"] = image.mode
155
+ metadata["format"] = image.format
156
+
157
+ return metadata
158
+ except Exception as e:
159
+ return {"error": f"Error getting file metadata: {str(e)}"}
160
+
161
+ @staticmethod
162
+ def _format_file_size(size_bytes: int) -> str:
163
+ """Format file size from bytes to human-readable format"""
164
+ for unit in ['B', 'KB', 'MB', 'GB']:
165
+ if size_bytes < 1024.0:
166
+ return f"{size_bytes:.2f} {unit}"
167
+ size_bytes /= 1024.0
168
+ return f"{size_bytes:.2f} TB"
services/quantized_model_loader.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
3
+ from configs.config import GPT4O_MODEL, LLAMA_MODEL
4
+
5
+ class QuantizedModelLoader:
6
+ """
7
+ This class loads a quantized model (either GPT-4.0 or Llama) at initialization.
8
+ The model is loaded in memory once and can be accessed via the `model` and `tokenizer` attributes.
9
+
10
+ Parameters:
11
+ model_type (str): A string indicating which model to load.
12
+ Expected values: "gpt4o" or "llama".
13
+ quant_4_bit (bool): If True, loads the model in 4-bit mode; otherwise, in 8-bit mode.
14
+ """
15
+ def __init__(self, model_type: str, quant_4_bit: bool = False):
16
+ self.quant_4_bit = quant_4_bit
17
+
18
+ # Select the model name from configuration.
19
+ if model_type.lower() == "gpt4o":
20
+ self.model_name = GPT4O_MODEL
21
+ elif model_type.lower() == "llama":
22
+ self.model_name = LLAMA_MODEL
23
+ else:
24
+ raise ValueError("Invalid model_type. Expected 'gpt4o' or 'llama'.")
25
+
26
+ self._initialize_model()
27
+
28
+ def _initialize_model(self):
29
+ # Set up quantization configuration.
30
+ if self.quant_4_bit:
31
+ quant_config = BitsAndBytesConfig(
32
+ load_in_4bit=True,
33
+ bnb_4bit_use_double_quant=True,
34
+ bnb_4bit_compute_dtype=torch.bfloat16,
35
+ bnb_4bit_quant_type="nf4"
36
+ )
37
+ else:
38
+ quant_config = BitsAndBytesConfig(
39
+ load_in_8bit=True,
40
+ bnb_8bit_compute_dtype=torch.bfloat16
41
+ )
42
+
43
+ # Load the tokenizer.
44
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_name, trust_remote_code=True)
45
+ self.tokenizer.pad_token = self.tokenizer.eos_token
46
+ self.tokenizer.padding_side = "right"
47
+
48
+ # Load the base model in quantized mode.
49
+ self.model = AutoModelForCausalLM.from_pretrained(
50
+ self.model_name,
51
+ quantization_config=quant_config,
52
+ device_map="auto"
53
+ )
54
+ # Update the tokenizer with the quantized model's pad token.
55
+ self.model.generation_config.pad_token_id = self.tokenizer.pad_token_id
56
+
57
+ print(f"Quantized model '{self.model_name}' loaded. Memory footprint: {self.model.get_memory_footprint() / 1e6:.1f} MB")
58
+
59
+ # Expose the class for import.
60
+ __all__ = ["QuantizedModelLoader"]
61
+
62
+ if __name__ == "__main__":
63
+ # Initialization of the models.
64
+ print("Initializing GPT-4.0 quantized model loader:")
65
+ gpt_loader = QuantizedModelLoader("gpt4o", quant_4_bit=False)
66
+ print("Initializing Llama quantized model loader:")
67
+ llama_loader = QuantizedModelLoader("llama", quant_4_bit=False)
services/system_prompts_service.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # services/system_prompts_service.py
2
+
3
+ from system_prompts.prompts_manager import get_system_prompt
4
+
5
+ class SystemPromptsService:
6
+ def __init__(self):
7
+ self.general_prompt = (
8
+ get_system_prompt(model_type="general")
9
+ )
10
+ self.study_prompt = (
11
+ get_system_prompt(model_type="study")
12
+ )
13
+
14
+ def get_prompt(self):
15
+ return [self.general_prompt, self.study_prompt]
services/ui_service-old2.py ADDED
@@ -0,0 +1,944 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import os
3
+ import time
4
+ import openai
5
+ import gradio as gr
6
+ import PyPDF2
7
+ import docx
8
+ import pandas as pd
9
+ import pptx
10
+ from PIL import Image
11
+ import io
12
+ import base64
13
+ import threading
14
+
15
+ from configs.config import claude
16
+ from .voice_interface import VoiceInterface
17
+ from .auth_service import authenticate
18
+
19
+
20
+ # Set the OpenAI API key from the environment variable
21
+ openai.api_key = os.getenv('OPENAI_API_KEY')
22
+ if not openai.api_key:
23
+ raise ValueError("OPENAI_API_KEY environment variable is not set. Please set it before running this script.")
24
+
25
+ # Initialize Voice Interface
26
+ voice_interface = VoiceInterface()
27
+
28
+ def custom_css():
29
+ css = """
30
+ /* Professional custom CSS with transitions, hover effects, and responsive design */
31
+ /* Your existing CSS remains here - unchanged */
32
+
33
+ /* Voice chat related CSS */
34
+ .voice-btn {
35
+ border-radius: 50%;
36
+ width: 45px;
37
+ height: 45px;
38
+ padding: 0;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ background-color: #000080;
43
+ color: white;
44
+ margin-right: 5px;
45
+ transition: all 0.3s ease;
46
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
47
+ }
48
+
49
+ .voice-btn:hover {
50
+ transform: scale(1.1);
51
+ background-color: #000066;
52
+ box-shadow: 0 4px 8px rgba(0,0,0,0.3);
53
+ }
54
+
55
+ .voice-btn.recording {
56
+ background-color: #cc0000;
57
+ animation: pulse 1.5s infinite;
58
+ }
59
+
60
+ @keyframes pulse {
61
+ 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(204, 0, 0, 0.7); }
62
+ 70% { transform: scale(1.1); box-shadow: 0 0 0 10px rgba(204, 0, 0, 0); }
63
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(204, 0, 0, 0); }
64
+ }
65
+
66
+ .voice-options {
67
+ margin-top: 10px;
68
+ padding: 10px;
69
+ border-radius: 8px;
70
+ background-color: rgba(0, 0, 128, 0.05);
71
+ }
72
+ """
73
+ return css
74
+
75
+ # ----------------- Login Form UI -----------------
76
+
77
+ def login_form_ui():
78
+ with gr.Blocks() as login_form:
79
+
80
+ # USIU logo
81
+ gr.HTML('''
82
+ <div class="logo-container">
83
+ <img src="week5/chatbot_prototype/images/usiu-logo.png" alt="USIU Logo" class="logo" />
84
+ </div>
85
+ ''')
86
+
87
+ gr.Markdown("## Please Login")
88
+ user_id_input = gr.Textbox(
89
+ placeholder="Enter your User ID",
90
+ label="User ID",
91
+ type="email",
92
+ autofocus=True
93
+ )
94
+ # Error message for invalid user ID
95
+ user_id_error = gr.Markdown("", visible=True)
96
+
97
+ password_input = gr.Textbox(
98
+ placeholder="Enter your Password",
99
+ label="Password",
100
+ type="password"
101
+ )
102
+ # Error message for invalid password
103
+ password_error = gr.Markdown("", visible=True)
104
+
105
+ login_btn = gr.Button("Login")
106
+ # Error message for invalid credentials
107
+ login_msg = gr.Markdown("")
108
+ proceed_btn = gr.Button("Proceed", visible=False)
109
+ back_btn = gr.Button("Back to Dashboard")
110
+ # Return the login form along with the necessary components in order.
111
+
112
+ return login_form, user_id_input, user_id_error, password_input, password_error, login_btn, login_msg, proceed_btn, back_btn
113
+
114
+ # ----------------- Authentication Logic -----------------
115
+
116
+ def show_success(message: str, duration: float = 10, visible: bool = True, title: str = "Success") -> None:
117
+ # Bright, material-style success colors:
118
+ styled_message = (
119
+ f'<span style="color: #2e7d32; background-color: #c8e6c9; padding: 10px; '
120
+ f'border-radius: 4px; display: block;">{message}</span>'
121
+ )
122
+ gr.Success(styled_message, duration=duration, visible=visible, title=title)
123
+ # Add a small delay to help the modal appear
124
+ time.sleep(0.1)
125
+
126
+ def show_error(message: str, duration: float = 13, visible: bool = True, title: str = "Error") -> None:
127
+ # Bright, material-style error colors:
128
+ styled_message = (
129
+ f'<span style="color: #c62828; background-color: #ffcdd2; padding: 10px; '
130
+ f'border-radius: 4px; display: block;">{message}</span>'
131
+ )
132
+ gr.Error(styled_message, duration=duration, visible=visible, title=title)
133
+ # Add a small delay to help the modal appear
134
+ time.sleep(0.1)
135
+
136
+ def handle_login(user_id, password):
137
+ """
138
+ Validates login fields and the User ID (which must end with '@usiu.ac.ke').
139
+ - If both fields are empty, a generic error modal is shown.
140
+ - If a specific field is empty, an error message for that field is returned.
141
+ - On successful authentication, a success modal pops up.
142
+
143
+ Returns a tuple of five outputs:
144
+ 1. Overall status message (displayed below the login button).
145
+ 2. User ID field-specific error (displayed below the User ID input).
146
+ 3. Password field-specific error (displayed below the Password input).
147
+ 4. A gr.update() call to control the visibility of the "Proceed" button.
148
+ 5. A gr.update() call to control the visibility of the "Login" button.
149
+ """
150
+ try:
151
+ user_id = user_id.strip() if user_id else ""
152
+ password = password.strip() if password else ""
153
+
154
+ # Both fields empty:
155
+ if user_id == "" and password == "":
156
+ show_error("Please enter your User ID and Password.")
157
+ return (
158
+ "<span style='color:#c62828;'>Please enter your User ID and Password.</span>",
159
+ "", # No field-specific error for User ID.
160
+ "", # No field-specific error for Password.
161
+ gr.update(visible=False),
162
+ gr.update(visible=True)
163
+ )
164
+ # User ID is empty:
165
+ elif user_id == "":
166
+ show_error("User ID is required.")
167
+ return (
168
+ "",
169
+ "<span style='color:#c62828;'>User ID is required.</span>",
170
+ "",
171
+ gr.update(visible=False),
172
+ gr.update(visible=True)
173
+ )
174
+ # Password is empty:
175
+ elif password == "":
176
+ show_error("Password is required.")
177
+ return (
178
+ "",
179
+ "",
180
+ "<span style='color:#c62828;'>Password is required.</span>",
181
+ gr.update(visible=False),
182
+ gr.update(visible=True)
183
+ )
184
+ else:
185
+ # Validate the User ID: must end with "@usiu.ac.ke"
186
+ email_regex = r'^[\w\.-]+@usiu\.ac\.ke$'
187
+ if not re.match(email_regex, user_id):
188
+ show_error("User ID must be a valid \"@usiu.ac.ke\" email address.")
189
+ return (
190
+ "",
191
+ "<span style='color:#c62828;'>User ID must be a valid @usiu.ac.ke email address.</span>",
192
+ "",
193
+ gr.update(visible=False),
194
+ gr.update(visible=True)
195
+ )
196
+ # Attempt authentication.
197
+ if authenticate(user_id, password):
198
+ show_success("Login successful! Click 'Proceed' to continue!", duration=10)
199
+ return (
200
+ "",
201
+ "",
202
+ "",
203
+ gr.update(visible=True), # Show Proceed button.
204
+ gr.update(visible=False) # Hide Login button.
205
+ )
206
+ else:
207
+ show_error("Invalid credentials. Please try again.")
208
+ return (
209
+ "<span style='color:#c62828;'>Invalid credentials. Please try again.</span>",
210
+ "",
211
+ "",
212
+ gr.update(visible=False),
213
+ gr.update(visible=True)
214
+ )
215
+ except NameError as e:
216
+ show_error(f"System error: {str(e)}. Please ensure required modules are imported.")
217
+ return (
218
+ "<span style='color:#c62828;'>System error encountered.</span>",
219
+ "",
220
+ "",
221
+ gr.update(visible=False),
222
+ gr.update(visible=True)
223
+ )
224
+
225
+ # ----------------- Voice Interaction Logic -----------------
226
+
227
+ def voice_button_click():
228
+ """Callback for the voice input button"""
229
+ voice_interface.start_recording()
230
+ # Wait for up to 10 seconds for voice input
231
+ text = voice_interface.get_text_from_speech(timeout=10)
232
+ voice_interface.stop_recording()
233
+ return text if text else ""
234
+
235
+ #############################
236
+ # READ FILE UTILITY
237
+ #############################
238
+ def read_file(file):
239
+ file_extension = os.path.splitext(file.name)[1].lower()
240
+
241
+ if file_extension == '.txt':
242
+ content = file.read().decode('utf-8')
243
+ elif file_extension == '.pdf':
244
+ pdf_reader = PyPDF2.PdfReader(file)
245
+ content = ''
246
+ for page in pdf_reader.pages:
247
+ text = page.extract_text()
248
+ if text:
249
+ content += text
250
+ elif file_extension in ['.docx', '.doc']:
251
+ doc = docx.Document(file)
252
+ content = "\n".join([para.text for para in doc.paragraphs])
253
+ elif file_extension in ['.xlsx', '.xls']:
254
+ df = pd.read_excel(file)
255
+ content = df.to_string()
256
+ elif file_extension in ['.pptx', '.ppt']:
257
+ prs = pptx.Presentation(file)
258
+ content = ''
259
+ for slide in prs.slides:
260
+ for shape in slide.shapes:
261
+ if hasattr(shape, 'text'):
262
+ content += shape.text + '\n'
263
+ elif file_extension in ['.png', '.jpg', '.jpeg', '.gif']:
264
+ image = Image.open(file)
265
+ buffered = io.BytesIO()
266
+ image.save(buffered, format="PNG")
267
+ # Create an image variation via OpenAI's API (example)
268
+ response = openai.Image.create_variation(
269
+ image=buffered,
270
+ n=1,
271
+ size="256x256"
272
+ )
273
+ image_description = response['data'][0]['url']
274
+ content = f"[Image uploaded. Description: {image_description}]"
275
+ else:
276
+ content = f"Unsupported file type: {file_extension}"
277
+
278
+ return content
279
+
280
+ #############################
281
+ # GLOBAL RETRIEVER SETUP (for RAG in general_chat_ui)
282
+ #############################
283
+ from vector_services.data_curator import DataCurator
284
+ curator = DataCurator(knowledge_base_dir="usiu-knowledge-base")
285
+ curator.load_vectorstore()
286
+ # GLOBAL_RETRIEVER enables RAG for general chat.
287
+ GLOBAL_RETRIEVER = curator.get_retriever()
288
+
289
+ #############################
290
+ # GENERAL CHAT UI (with RAG)
291
+ #############################
292
+ def general_chat_ui(system_prompt: str, model: str) -> None:
293
+ """
294
+ Creates a Gradio ChatInterface for general academic chat with voice capabilities.
295
+ """
296
+ def general_chat(message: str, chat_history, file, play_response=False) -> str:
297
+ # Ensure chat_history is a list.
298
+ if chat_history is None:
299
+ chat_history = []
300
+
301
+ # If a file is uploaded and the message is empty, replace the empty message with the file name.
302
+ if not message.strip() and file is not None:
303
+ message = f"Uploaded file: {os.path.basename(file.name)}"
304
+
305
+ # If this is a new conversation, insert a conversation header.
306
+ if not chat_history:
307
+ date_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
308
+ conv_header = {
309
+ "role": "header",
310
+ "content": (
311
+ f"<div style='text-align: center; font-weight: bold; font-size: 16px; color: #000;'>"
312
+ f"Conversation started on {date_str}"
313
+ f"</div>"
314
+ ),
315
+ "files": []
316
+ }
317
+ chat_history.insert(0, conv_header) # Insert at the beginning
318
+
319
+ # Start with the initial system prompt.
320
+ messages = [
321
+ {"role": "system", "content": system_prompt, "files": []},
322
+ ]
323
+
324
+ # Add chat history to messages (all messages, including header).
325
+ if chat_history:
326
+ # If the first element is a tuple/list with two elements, assume pair format.
327
+ if isinstance(chat_history[0], (tuple, list)) and len(chat_history[0]) == 2:
328
+ for human, assistant in chat_history:
329
+ messages.append({"role": "user", "content": human, "files": []})
330
+ messages.append({"role": "assistant", "content": assistant, "files": []})
331
+ # Otherwise, if they are dicts, include all messages.
332
+ elif isinstance(chat_history[0], dict):
333
+ for msg in chat_history:
334
+ if msg is None:
335
+ continue
336
+ # Skip header messages when sending to API.
337
+ if msg.get("role") == "header":
338
+ continue
339
+ # Ensure each message has a files field.
340
+ msg_to_send = dict(msg)
341
+ if "files" not in msg_to_send or msg_to_send["files"] is None:
342
+ msg_to_send["files"] = []
343
+ messages.append(msg_to_send)
344
+
345
+ # If a file is uploaded, add its content to the messages.
346
+ if file is not None:
347
+ try:
348
+ file_content = read_file(file)
349
+ messages.append({
350
+ "role": "user",
351
+ "content": f"Here's the content of the uploaded file:\n\n{file_content}",
352
+ "files": []
353
+ })
354
+ except Exception as e:
355
+ messages.append({
356
+ "role": "user",
357
+ "content": f"Error reading file: {str(e)}",
358
+ "files": []
359
+ })
360
+
361
+ # RAG: Retrieve additional context using GLOBAL_RETRIEVER if available.
362
+ if GLOBAL_RETRIEVER is not None:
363
+ try:
364
+ docs = GLOBAL_RETRIEVER.get_relevant_documents(message)
365
+ if docs:
366
+ context_text = "\n\n".join([doc.page_content for doc in docs])
367
+ # Insert the retrieved context as an additional system message.
368
+ messages.insert(1, {
369
+ "role": "system",
370
+ "content": f"Additional Context:\n{context_text}",
371
+ "files": []
372
+ })
373
+ except Exception as e:
374
+ print("RAG retrieval failed:", e)
375
+
376
+ # Append the current user message to chat_history as a dictionary for consistency.
377
+ chat_history.append({"role": "user", "content": message, "files": []})
378
+
379
+ # Also add the user message to the messages list (so it is sent to the API).
380
+ messages.append({"role": "user", "content": message, "files": []})
381
+
382
+ print("General Enquiries - Conversation with context:")
383
+ print(messages)
384
+
385
+ # Call OpenAI's ChatCompletion with streaming enabled.
386
+ completion = openai.ChatCompletion.create(
387
+ model=model,
388
+ messages=messages,
389
+ max_tokens=2_000,
390
+ temperature=0.7,
391
+ stream=True,
392
+ )
393
+
394
+ response = ""
395
+ for chunk in completion:
396
+ token = chunk.choices[0].delta.get("content", "")
397
+ response += token
398
+ yield response
399
+
400
+ # After the response is completely generated, append a timestamp.
401
+ timestamp = time.strftime("%I:%M %p", time.localtime())
402
+ final_response = response + f"\n<span style='font-size:11px; color:#999;'>{timestamp}</span>"
403
+
404
+ # If voice playback is enabled, read the response aloud in a separate thread
405
+ if play_response:
406
+ threading.Thread(
407
+ target=voice_interface.text_to_speech,
408
+ args=(response,),
409
+ daemon=True
410
+ ).start()
411
+
412
+ yield final_response
413
+
414
+ with gr.Blocks() as interface:
415
+ # Create the main chatbot UI
416
+ chatbot = gr.Chatbot(
417
+ [],
418
+ elem_id="general-chatbot",
419
+ avatar_images=(None, "https://i.imgur.com/KLhAVLW.png"),
420
+ bubble_full_width=False,
421
+ height=500
422
+ )
423
+
424
+ with gr.Row():
425
+ with gr.Column(scale=10):
426
+ msg = gr.Textbox(
427
+ show_label=False,
428
+ placeholder="Type your message here...",
429
+ container=False,
430
+ lines=1
431
+ )
432
+ with gr.Column(scale=1, min_width=50):
433
+ voice_input_btn = gr.Button("🎤", elem_classes="voice-btn")
434
+ with gr.Column(scale=1, min_width=60):
435
+ send_btn = gr.Button("Send")
436
+
437
+ with gr.Accordion("Additional Options", open=False, elem_classes="additional-inputs-accordion"):
438
+ with gr.Row():
439
+ file_upload = gr.File(label="Upload a file (optional)", elem_classes="file-upload", scale=2)
440
+ voice_output = gr.Checkbox(label="Enable Voice Responses", value=False, scale=1, elem_classes="voice-options")
441
+
442
+ # Set up event handlers
443
+ voice_input_btn.click(
444
+ voice_button_click,
445
+ outputs=[msg]
446
+ )
447
+
448
+ msg.submit(
449
+ general_chat,
450
+ [msg, chatbot, file_upload, voice_output],
451
+ [chatbot]
452
+ ).then(
453
+ lambda: "", None, [msg]
454
+ )
455
+
456
+ send_btn.click(
457
+ general_chat,
458
+ [msg, chatbot, file_upload, voice_output],
459
+ [chatbot]
460
+ ).then(
461
+ lambda: "", None, [msg]
462
+ )
463
+
464
+ # Add JavaScript for voice button behavior
465
+ gr.HTML("""
466
+ <script>
467
+ document.addEventListener('DOMContentLoaded', function() {
468
+ // Find voice button after a delay to ensure it's loaded
469
+ setTimeout(function() {
470
+ const voiceBtn = document.querySelector('.voice-btn');
471
+ if (voiceBtn) {
472
+ voiceBtn.addEventListener('click', function() {
473
+ this.classList.add('recording');
474
+
475
+ // Remove recording class after 10 seconds (max recording time)
476
+ setTimeout(() => {
477
+ this.classList.remove('recording');
478
+ }, 10000);
479
+ });
480
+ }
481
+ }, 2000);
482
+ });
483
+ </script>
484
+ """)
485
+
486
+ return interface
487
+
488
+ #############################
489
+ # STUDY SUPPORT UI (without RAG)
490
+ #############################
491
+ def study_support_ui(system_prompt: str, model: str) -> None:
492
+ """
493
+ Creates a Gradio interface for study support with voice capabilities.
494
+ """
495
+ def study_support_chat(message: str, chat_history, file, play_response=False) -> str:
496
+ # Ensure chat_history is a list.
497
+ if chat_history is None:
498
+ chat_history = []
499
+
500
+ # If a file is uploaded and the message is empty, replace the empty message with the file name.
501
+ if not message.strip() and file is not None:
502
+ message = f"Uploaded file: {os.path.basename(file.name)}"
503
+
504
+ # If this is a new conversation, insert a conversation header.
505
+ if not chat_history:
506
+ date_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
507
+ conv_header = {
508
+ "role": "header",
509
+ "content": (
510
+ f"<div style='text-align: center; font-weight: bold; font-size: 16px; color: #000;'>"
511
+ f"Conversation started on {date_str}"
512
+ f"</div>"
513
+ )
514
+ }
515
+ chat_history.insert(0, conv_header)
516
+
517
+ # Build the messages list that will be sent to the API.
518
+ messages = []
519
+ if chat_history:
520
+ # If chat_history items are tuples/lists with two elements, unpack them.
521
+ if isinstance(chat_history[0], (tuple, list)) and len(chat_history[0]) == 2:
522
+ for human, assistant in chat_history:
523
+ messages.append({"role": "user", "content": human})
524
+ messages.append({"role": "assistant", "content": assistant})
525
+ # Otherwise, if they are dicts, include only "role" and "content" for non-system messages.
526
+ elif isinstance(chat_history[0], dict):
527
+ for msg in chat_history:
528
+ if msg is None:
529
+ continue
530
+ if msg.get("role") == "header":
531
+ continue # Skip header messages.
532
+ msg_to_send = {"role": msg.get("role"), "content": msg.get("content")}
533
+ messages.append(msg_to_send)
534
+
535
+ # Add the current message.
536
+ messages.append({"role": "user", "content": message})
537
+
538
+ # If a file is uploaded, add its content.
539
+ if file is not None:
540
+ try:
541
+ file_content = read_file(file)
542
+ messages.append({
543
+ "role": "user",
544
+ "content": f"Here's the content of the uploaded file:\n\n{file_content}"
545
+ })
546
+ except Exception as e:
547
+ messages.append({
548
+ "role": "user",
549
+ "content": f"Error reading file: {str(e)}"
550
+ })
551
+
552
+ # Add current user message to chat history for display
553
+ chat_history.append({"role": "user", "content": message})
554
+
555
+ # Implement a retry mechanism to handle overloaded errors.
556
+ max_retries = 7
557
+ delay = 3 # seconds
558
+ attempt = 0
559
+
560
+ while attempt < max_retries:
561
+ try:
562
+ result = claude.messages.stream(
563
+ model=model,
564
+ max_tokens=64_000,
565
+ system=system_prompt,
566
+ messages=messages,
567
+ extra_query={"extended_thinking": True}
568
+ )
569
+ response = ""
570
+ with result as stream:
571
+ for text in stream.text_stream:
572
+ response += text
573
+ yield response # Yield incremental responses.
574
+
575
+ # After the response is completely generated, append a timestamp.
576
+ timestamp = time.strftime("%I:%M %p", time.localtime())
577
+ final_response = response + f"\n<span style='font-size:11px; color:#999;'>{timestamp}</span>"
578
+
579
+ # If voice playback is enabled, read the response aloud in a separate thread
580
+ if play_response:
581
+ threading.Thread(
582
+ target=voice_interface.text_to_speech,
583
+ args=(response,),
584
+ daemon=True
585
+ ).start()
586
+
587
+ yield final_response
588
+ break # Exit the retry loop if successful.
589
+ except Exception as e:
590
+ # Check if error indicates the API is overloaded.
591
+ if "overloaded" in str(e).lower():
592
+ attempt += 1
593
+ yield f"Service unavailable. Retrying in {delay} seconds... (attempt {attempt}/{max_retries})\n"
594
+ time.sleep(delay)
595
+ else:
596
+ print(f"An error occurred: {str(e)}")
597
+ break
598
+ else:
599
+ yield "The service is currently unavailable. Please try again later."
600
+
601
+ with gr.Blocks() as interface:
602
+ # Create the main chatbot UI
603
+ chatbot = gr.Chatbot(
604
+ [],
605
+ elem_id="study-chatbot",
606
+ avatar_images=(None, "https://i.imgur.com/KLhAVLW.png"),
607
+ bubble_full_width=False,
608
+ height=500
609
+ )
610
+
611
+ with gr.Row():
612
+ with gr.Column(scale=10):
613
+ msg = gr.Textbox(
614
+ show_label=False,
615
+ placeholder="Type your message here...",
616
+ container=False,
617
+ lines=1
618
+ )
619
+ with gr.Column(scale=1, min_width=50):
620
+ voice_input_btn = gr.Button("🎤", elem_classes="voice-btn")
621
+ with gr.Column(scale=1, min_width=60):
622
+ send_btn = gr.Button("Send")
623
+
624
+ with gr.Accordion("Additional Options", open=False, elem_classes="additional-inputs-accordion"):
625
+ with gr.Row():
626
+ file_upload = gr.File(label="Upload a file (optional)", elem_classes="file-upload", scale=2)
627
+ voice_output = gr.Checkbox(label="Enable Voice Responses", value=False, scale=1, elem_classes="voice-options")
628
+
629
+ # Set up event handlers
630
+ voice_input_btn.click(
631
+ voice_button_click,
632
+ outputs=[msg]
633
+ )
634
+
635
+ msg.submit(
636
+ study_support_chat,
637
+ [msg, chatbot, file_upload, voice_output],
638
+ [chatbot]
639
+ ).then(
640
+ lambda: "", None, [msg]
641
+ )
642
+
643
+ send_btn.click(
644
+ study_support_chat,
645
+ [msg, chatbot, file_upload, voice_output],
646
+ [chatbot]
647
+ ).then(
648
+ lambda: "", None, [msg]
649
+ )
650
+
651
+ # Add JavaScript for voice button behavior
652
+ gr.HTML("""
653
+ <script>
654
+ document.addEventListener('DOMContentLoaded', function() {
655
+ // Find voice button after a delay to ensure it's loaded
656
+ setTimeout(function() {
657
+ const voiceBtn = document.querySelector('#study-chatbot + div .voice-btn');
658
+ if (voiceBtn) {
659
+ voiceBtn.addEventListener('click', function() {
660
+ this.classList.add('recording');
661
+
662
+ // Remove recording class after 10 seconds (max recording time)
663
+ setTimeout(() => {
664
+ this.classList.remove('recording');
665
+ }, 10000);
666
+ });
667
+ }
668
+ }, 2000);
669
+ });
670
+ </script>
671
+ """)
672
+
673
+ return interface
674
+
675
+ # ---------- Dashboard UI ----------
676
+ def dashboard_ui(general_chat_prompt: str, general_model: str, study_prompt: str, study_model: str):
677
+ """
678
+ Creates a dashboard with professional styling, two main menus (General Chat and Study Support),
679
+ and smooth transitions between views. For Study Support, a login form (with authentication)
680
+ is shown before revealing the chat interface.
681
+ """
682
+ # Retrieve the existing chat interfaces.
683
+ general_chat_component = general_chat_ui(general_chat_prompt, general_model)
684
+ study_support_component = study_support_ui(study_prompt, study_model)
685
+ # Retrieve the login form.
686
+ (login_form_component, user_id_input, user_id_error, password_input, password_error,
687
+ login_btn, login_msg, proceed_btn, login_back_btn) = login_form_ui()
688
+
689
+ with gr.Blocks(css=custom_css()) as dashboard:
690
+ # Inject JavaScript for animations and loading effects
691
+ gr.HTML('''
692
+ <script>
693
+ document.addEventListener("DOMContentLoaded", function() {
694
+ // Create and style the spinner element
695
+ let spinner = document.createElement("div");
696
+ spinner.className = "loader";
697
+ spinner.innerHTML = '<div class="cube"></div>';
698
+ document.body.appendChild(spinner);
699
+
700
+ // Show the spinner when needed
701
+ spinner.style.display = "none"; // Initially hidden
702
+
703
+ // Function to show spinner
704
+ function showSpinner() {
705
+ spinner.style.display = "block";
706
+ }
707
+
708
+ // Function to hide spinner
709
+ function hideSpinner() {
710
+ spinner.style.display = "none";
711
+ }
712
+
713
+ // Function to handle animation transitions
714
+ function setupAnimations() {
715
+ // Get all containers that might be animated
716
+ const containers = document.querySelectorAll('.container, .chat-container');
717
+
718
+ // Add transition end listener to each container
719
+ containers.forEach(container => {
720
+ container.addEventListener('animationend', function(e) {
721
+ // When animation completes, remove the animation class
722
+ if (e.animationName.includes('fadeIn')) {
723
+ this.classList.remove('animate-fadeInUp', 'animate-fadeInRight', 'animate-fadeInLeft');
724
+ }
725
+ });
726
+ });
727
+ }
728
+
729
+ // Setup animations after a short delay to ensure DOM is ready
730
+ setTimeout(setupAnimations, 2000);
731
+
732
+ // Add event listeners to all buttons that should trigger the spinner
733
+ setTimeout(function(){
734
+ // Login and Proceed buttons with spinner
735
+ const loginBtn = document.querySelector('button:contains("Login")');
736
+ const proceedBtn = document.querySelector('button:contains("Proceed")');
737
+
738
+ if (loginBtn) {
739
+ loginBtn.addEventListener("click", function() {
740
+ showSpinner(); // Show spinner when login button is clicked
741
+
742
+ if (!this.classList.contains("loading")) {
743
+ const originalText = this.textContent;
744
+ this.setAttribute("data-original-text", originalText);
745
+ this.textContent = "";
746
+ this.classList.add("loading");
747
+
748
+ // Create spinner inside button
749
+ const btnSpinner = document.createElement("div");
750
+ btnSpinner.className = "btn-spinner";
751
+ btnSpinner.style.width = "20px";
752
+ btnSpinner.style.height = "20px";
753
+ btnSpinner.style.border = "3px solid rgba(255,255,255,0.3)";
754
+ btnSpinner.style.borderRadius = "50%";
755
+ btnSpinner.style.borderTopColor = "#fff";
756
+ btnSpinner.style.animation = "spin 1s linear infinite";
757
+ btnSpinner.style.display = "inline-block";
758
+ btnSpinner.style.verticalAlign = "middle";
759
+ this.appendChild(btnSpinner);
760
+
761
+ // Reset button after 3 seconds if no response
762
+ setTimeout(() => {
763
+ if (this.classList.contains("loading")) {
764
+ this.classList.remove("loading");
765
+ this.textContent = this.getAttribute("data-original-text");
766
+ }
767
+ hideSpinner(); // Hide spinner after timeout
768
+ }, 3000);
769
+ }
770
+ });
771
+ }
772
+
773
+ if (proceedBtn) {
774
+ proceedBtn.addEventListener("click", function() {
775
+ showSpinner(); // Show spinner when proceed button is clicked
776
+
777
+ if (!this.classList.contains("loading")) {
778
+ const originalText = this.textContent;
779
+ this.setAttribute("data-original-text", originalText);
780
+ this.textContent = "";
781
+ this.classList.add("loading");
782
+
783
+ // Create spinner inside button
784
+ const btnSpinner = document.createElement("div");
785
+ btnSpinner.className = "btn-spinner";
786
+ btnSpinner.style.width = "20px";
787
+ btnSpinner.style.height = "20px";
788
+ btnSpinner.style.border = "3px solid rgba(255,255,255,0.3)";
789
+ btnSpinner.style.borderRadius = "50%";
790
+ btnSpinner.style.borderTopColor = "#fff";
791
+ btnSpinner.style.animation = "spin 1s linear infinite";
792
+ btnSpinner.style.display = "inline-block";
793
+ btnSpinner.style.verticalAlign = "middle";
794
+ this.appendChild(btnSpinner);
795
+
796
+ // Reset button after 3 seconds if no response
797
+ setTimeout(() => {
798
+ if (this.classList.contains("loading")) {
799
+ this.classList.remove("loading");
800
+ this.textContent = this.getAttribute("data-original-text");
801
+ }
802
+ hideSpinner(); // Hide spinner after timeout
803
+ }, 3000);
804
+ }
805
+ });
806
+ }
807
+
808
+ // Scale effect for dashboard buttons
809
+ const dashboardButtons = document.querySelectorAll('.menu-button, button:contains("Back to Dashboard")');
810
+ dashboardButtons.forEach(function(btn) {
811
+ btn.addEventListener("click", function() {
812
+ this.style.transform = "scale(1.1)";
813
+ setTimeout(() => {
814
+ this.style.transform = "";
815
+ }, 200);
816
+ });
817
+ });
818
+ }, 2000);
819
+
820
+ // Add keyframes for spinner animation
821
+ const style = document.createElement('style');
822
+ style.textContent = `
823
+ @keyframes spin {
824
+ 0% { transform: rotate(0deg); }
825
+ 100% { transform: rotate(360deg); }
826
+ }
827
+ `;
828
+ document.head.appendChild(style);
829
+ });
830
+ </script>
831
+ ''')
832
+
833
+ # Logo for all pages - positioned in top-left corner
834
+ gr.HTML('''
835
+ <div class="logo-dashboard">
836
+ <!-- USIU Logo Placeholder - Replace the src with actual logo path -->
837
+ <img src="week5/chatbot_prototype/images/usiu-logo.png" alt="USIU Logo" class="logo" />
838
+ </div>
839
+ ''')
840
+
841
+ # Define containers for different views with initial animation state
842
+ dashboard_container = gr.Column(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInUp")
843
+ general_container = gr.Column(visible=False, elem_classes="chat-container pre-animation")
844
+ login_container = gr.Column(visible=False, elem_classes="container floating-card login-form-container pre-animation")
845
+ study_container = gr.Column(visible=False, elem_classes="chat-container pre-animation")
846
+
847
+ # ------------- Dashboard View -------------
848
+ with dashboard_container:
849
+ gr.Markdown("<h1 class='with-logo'>AI Assistance Dashboard</h1>")
850
+ with gr.Column(elem_classes="dashboard-menu"):
851
+ gen_button = gr.Button("General Enquiries", elem_classes="menu-button")
852
+ study_button = gr.Button("Study with AI", elem_classes="menu-button")
853
+
854
+ # ------------- General Academic Chat View -------------
855
+ with general_container:
856
+ back_gen = gr.Button("Back to Dashboard")
857
+ general_chat_component.render()
858
+
859
+ # ------------- Login Form (for Study Support) View -------------
860
+ with login_container:
861
+ login_form_component.render()
862
+
863
+ # ------------- Study Support Chat View -------------
864
+ with study_container:
865
+ back_study = gr.Button("Back to Dashboard")
866
+ study_support_component.render()
867
+
868
+ # ------------- Navigation Callbacks -------------
869
+ # When "General Academic Chat" is selected from the dashboard.
870
+ gen_button.click(
871
+ lambda: [
872
+ gr.update(visible=False),
873
+ gr.update(visible=True, elem_classes="chat-container animate-fadeInRight"),
874
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
875
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
876
+ ],
877
+ outputs=[dashboard_container, general_container, login_container, study_container]
878
+ )
879
+
880
+ # When "Study Support Chat" is selected from the dashboard, show the login view with animation.
881
+ study_button.click(
882
+ lambda: [
883
+ gr.update(visible=False),
884
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
885
+ gr.update(visible=True, elem_classes="container floating-card login-form-container animate-fadeInRight"),
886
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
887
+ ],
888
+ outputs=[dashboard_container, general_container, login_container, study_container]
889
+ )
890
+
891
+ # "Back to Dashboard" buttons with animation.
892
+ back_gen.click(
893
+ lambda: [
894
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
895
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
896
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
897
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
898
+ ],
899
+ outputs=[dashboard_container, general_container, login_container, study_container]
900
+ )
901
+
902
+ back_study.click(
903
+ lambda: [
904
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
905
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
906
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
907
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
908
+ ],
909
+ outputs=[dashboard_container, general_container, login_container, study_container]
910
+ )
911
+
912
+ login_back_btn.click(
913
+ lambda: [
914
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
915
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
916
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
917
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
918
+ ],
919
+ outputs=[dashboard_container, general_container, login_container, study_container]
920
+ )
921
+
922
+ # ------------- Authentication Logic -------------
923
+ # ------------- Authentication Logic -------------
924
+ login_btn.click(
925
+ handle_login,
926
+ inputs=[user_id_input, password_input],
927
+ outputs=[login_msg, user_id_error, password_error, proceed_btn, login_btn]
928
+ )
929
+
930
+ proceed_btn.click(
931
+ lambda: [
932
+ gr.update(visible=False),
933
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
934
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
935
+ gr.update(visible=True, elem_classes="chat-container animate-fadeInRight")
936
+ ],
937
+ outputs=[dashboard_container, general_container, login_container, study_container]
938
+ )
939
+
940
+ # Register the cleanup function to run when the program exits
941
+ import atexit
942
+ atexit.register(voice_interface.cleanup)
943
+
944
+ return dashboard.queue()
services/ui_service-old3.py ADDED
@@ -0,0 +1,1527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import os
3
+ import time
4
+ import openai
5
+ import gradio as gr
6
+ import PyPDF2
7
+ import docx
8
+ import pandas as pd
9
+ import pptx
10
+ from PIL import Image
11
+ import io
12
+ import base64
13
+
14
+ from configs.config import claude
15
+ from .auth_service import authenticate
16
+
17
+
18
+ # Set the OpenAI API key from the environment variable
19
+ openai.api_key = os.getenv('OPENAI_API_KEY')
20
+ if not openai.api_key:
21
+ raise ValueError("OPENAI_API_KEY environment variable is not set. Please set it before running this script.")
22
+
23
+ def custom_css():
24
+ css = """
25
+ /* Professional custom CSS with transitions, hover effects, and responsive design */
26
+ body {
27
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
28
+ background-color: #f8f9fa;
29
+ margin: 0;
30
+ padding: 0;
31
+ width: 100%;
32
+ overflow-x: hidden; /* Prevent horizontal scrolling */
33
+ }
34
+ h1, h2, h3 {
35
+ text-align: center;
36
+ color: #495057;
37
+ }
38
+ .gr-button {
39
+ transition: background-color 0.3s ease, transform 0.3s ease;
40
+ border-radius: 4px;
41
+ /* USIU-Africa theme color for buttons */
42
+ background-color: #000080;
43
+ color: white;
44
+ }
45
+ .gr-button:hover {
46
+ background-color: #000066;
47
+ color: #fff;
48
+ transform: scale(1.03);
49
+ }
50
+ .container {
51
+ transition: opacity 0.5s ease-in-out;
52
+ padding: 4px;
53
+ }
54
+
55
+ /* Glassmorphism effect for floating cards */
56
+ .floating-card {
57
+ background: rgba(255, 255, 255, 0.85);
58
+ border-radius: 12px;
59
+ box-shadow:
60
+ 0 10px 25px rgba(0,0,0,0.08),
61
+ 0 6px 10px rgba(0,0,0,0.12),
62
+ 0 3px 3px rgba(0,0,0,0.15);
63
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
64
+ padding: 25px;
65
+ margin: 15px auto; /* Reduced margin */
66
+ backdrop-filter: blur(10px);
67
+ border: 1px solid rgba(255,255,255,0.25);
68
+ width: 95%; /* Increased width */
69
+ max-width: 98%; /* Increased max-width */
70
+ }
71
+
72
+ .floating-card:hover {
73
+ box-shadow:
74
+ 0 15px 30px rgba(0,0,0,0.15),
75
+ 0 10px 10px rgba(0,0,0,0.18);
76
+ transform: translateY(-5px);
77
+ background: rgba(255, 255, 255, 0.9);
78
+ }
79
+
80
+ /* Login form styling with responsiveness */
81
+ .login-form-container {
82
+ width: 98% !important; /* Increased width for mobile */
83
+ max-width: 500px !important;
84
+ margin: 30px auto !important; /* Reduced top/bottom margin */
85
+ background: rgba(255, 255, 255, 0.88) !important;
86
+ backdrop-filter: blur(12px) !important;
87
+ border: 1px solid rgba(255, 255, 255, 0.3) !important;
88
+ border-top: 3px solid #000080 !important;
89
+ padding: 20px 15px !important; /* Added horizontal padding */
90
+ }
91
+
92
+ /* Dashboard container styling with responsiveness */
93
+ .dashboard-card {
94
+ width: 98% !important; /* Take almost full width on mobile */
95
+ max-width: 700px !important;
96
+ margin: 20px auto !important; /* Reduced margin */
97
+ background: rgba(255, 255, 255, 0.88) !important;
98
+ backdrop-filter: blur(12px) !important;
99
+ height: auto !important;
100
+ min-height: 500px !important;
101
+ border: 1px solid rgba(255, 255, 255, 0.3) !important;
102
+ border-top: 3px solid #000080 !important;
103
+ opacity: 0;
104
+ animation: dashboardFadeIn 1.5s forwards ease-in-out;
105
+ padding: 20px 10px !important; /* Reduced horizontal padding */
106
+ }
107
+
108
+ /* A subtle gradient background to the cards */
109
+ .dashboard-card, .login-form-container {
110
+ background: linear-gradient(
111
+ 135deg,
112
+ rgba(255, 255, 255, 0.9) 0%,
113
+ rgba(255, 255, 255, 0.8) 100%
114
+ ) !important;
115
+ }
116
+
117
+ /* Logo container styling with responsive adjustments */
118
+ .logo-container {
119
+ text-align: center;
120
+ margin-bottom: 25px;
121
+ position: relative;
122
+ width: 100%;
123
+ }
124
+
125
+ .logo {
126
+ max-height: 80px;
127
+ max-width: 90%; /* Increased max-width */
128
+ margin: 0 auto;
129
+ display: block;
130
+ }
131
+
132
+ .logo-dashboard {
133
+ position: absolute;
134
+ top: 15px;
135
+ left: 15px;
136
+ max-height: 60px;
137
+ max-width: 35%; /* Increased max-width */
138
+ z-index: 100;
139
+ opacity: 0;
140
+ animation: logoFadeIn 1.8s forwards ease-in-out;
141
+ }
142
+
143
+ .with-logo {
144
+ margin-top: 10px;
145
+ }
146
+
147
+ /* Chat UI container with extended width and responsiveness */
148
+ .chat-container {
149
+ max-width: 1000px;
150
+ width: 98%; /* Increased width for mobile */
151
+ margin: 0 auto;
152
+ opacity: 0;
153
+ transform: translateY(20px);
154
+ animation: fadeInUp 0.7s forwards;
155
+ }
156
+
157
+ /* Increase chat window height with responsive adjustment */
158
+ .chatbot-container, .gradio-container .chat {
159
+ height: 80vh !important;
160
+ max-height: 900px !important;
161
+ width: 98% !important; /* Take up more width */
162
+ margin: 0 auto !important;
163
+ }
164
+
165
+ /* Make Gradio container take full width on mobile */
166
+ .gradio-container {
167
+ width: 100% !important;
168
+ max-width: 100% !important;
169
+ margin: 0 !important;
170
+ padding: 0 !important;
171
+ }
172
+
173
+ /* File input adjustment - restore default styling */
174
+ .file-upload .p-2 {
175
+ padding: inherit !important;
176
+ }
177
+
178
+ /* Accordion button adjustment - remove forced padding to restore default behavior */
179
+ .accordion-button {
180
+ padding: inherit !important;
181
+ }
182
+
183
+ /* Fix for accordion collapsed state */
184
+ .accordion-button.collapsed {
185
+ /* Reset any custom styles when collapsed */
186
+ padding: initial !important;
187
+ height: auto !important;
188
+ min-height: initial !important;
189
+ }
190
+
191
+ /* Allow accordion and file input to use Gradio's default styling */
192
+ .file-upload, .accordion {
193
+ /* Restore default Gradio styling */
194
+ margin: initial !important;
195
+ padding: initial !important;
196
+ }
197
+
198
+ /* Restore default styling for chat action buttons (clear, retry, undo) */
199
+ .chat-buttons button,
200
+ .chat-buttons .gr-button,
201
+ button[aria-label="Clear"],
202
+ button[aria-label="Retry"],
203
+ button[aria-label="Undo"] {
204
+ padding: initial !important;
205
+ height: auto !important;
206
+ min-height: initial !important;
207
+ width: auto !important;
208
+ min-width: initial !important;
209
+ background-color: initial !important;
210
+ color: initial !important;
211
+ transform: none !important;
212
+ }
213
+
214
+ /* Hover effect for chat buttons */
215
+ .chat-buttons button:hover,
216
+ .chat-buttons .gr-button:hover,
217
+ button[aria-label="Clear"]:hover,
218
+ button[aria-label="Retry"]:hover,
219
+ button[aria-label="Undo"]:hover {
220
+ transform: none !important;
221
+ background-color: initial !important;
222
+ background-image: none !important;
223
+ }
224
+
225
+ /* Dashboard menu buttons styling with responsiveness */
226
+ .dashboard-menu {
227
+ display: flex;
228
+ flex-direction: column;
229
+ align-items: center;
230
+ justify-content: center;
231
+ min-height: 60vh;
232
+ gap: 8px;
233
+ opacity: 0;
234
+ animation: menuFadeIn 3s forwards ease-in-out;
235
+ animation-delay: 0.7s;
236
+ padding: 15px 0; /* Reduced padding */
237
+ width: 100%; /* Full width */
238
+ }
239
+
240
+ .menu-button {
241
+ width: 95% !important; /* Increased width for mobile */
242
+ max-width: 300px !important;
243
+ height: auto !important;
244
+ min-height: 60px !important;
245
+ font-size: 16px !important;
246
+ font-weight: 500 !important;
247
+ margin: 6px auto !important; /* Reduced margin */
248
+ border-radius: 8px !important;
249
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
250
+ transition: transform 0.3s ease, box-shadow 0.3s ease !important;
251
+ background-color: #000080 !important;
252
+ color: white !important;
253
+ opacity: 0;
254
+ animation: buttonsFadeIn 0.7s forwards ease-in-out;
255
+ animation-delay: calc(var(--button-index, 0) * 0.3s + 1s);
256
+ padding: 10px 15px !important;
257
+ display: flex !important;
258
+ align-items: center !important;
259
+ justify-content: center !important;
260
+ text-align: center !important;
261
+ word-wrap: break-word !important;
262
+ }
263
+
264
+ .menu-button:hover {
265
+ transform: scale(1.02);
266
+ background-image: linear-gradient(to bottom right, #000080, #0000cc);
267
+ transition: transform 0.5s ease;
268
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important;
269
+ }
270
+
271
+ /* 3D Loader CSS with responsive sizing */
272
+ .loader {
273
+ width: 40px; /* Reduced size */
274
+ height: 40px; /* Reduced size */
275
+ perspective: 100px;
276
+ position: fixed;
277
+ top: 50%;
278
+ left: 50%;
279
+ transform: translate(-50%, -50%);
280
+ z-index: 9999;
281
+ display: none;
282
+ }
283
+ .cube {
284
+ width: 100%;
285
+ height: 100%;
286
+ background: #000080;
287
+ transform: rotateX(0deg) rotateY(0deg);
288
+ animation: spinCube 1.5s infinite ease-in-out;
289
+ }
290
+ @keyframes spinCube {
291
+ 0% { transform: rotateX(0deg) rotateY(0deg); }
292
+ 25% { transform: rotateX(90deg) rotateY(0deg); }
293
+ 50% { transform: rotateX(90deg) rotateY(90deg); }
294
+ 75% { transform: rotateX(0deg) rotateY(90deg); }
295
+ 100% { transform: rotateX(0deg) rotateY(0deg); }
296
+ }
297
+
298
+ /* Dashboard specific fade-in animations */
299
+ @keyframes dashboardFadeIn {
300
+ 0% {
301
+ opacity: 0;
302
+ transform: translateY(30px);
303
+ }
304
+ 100% {
305
+ opacity: 1;
306
+ transform: translateY(0);
307
+ }
308
+ }
309
+
310
+ @keyframes logoFadeIn {
311
+ 0% {
312
+ opacity: 0;
313
+ transform: translateY(-20px);
314
+ }
315
+ 100% {
316
+ opacity: 1;
317
+ transform: translateY(0);
318
+ }
319
+ }
320
+
321
+ @keyframes menuFadeIn {
322
+ 0% {
323
+ opacity: 0;
324
+ transform: scale(0.95);
325
+ }
326
+ 100% {
327
+ opacity: 1;
328
+ transform: scale(1);
329
+ }
330
+ }
331
+
332
+ @keyframes buttonsFadeIn {
333
+ 0% {
334
+ opacity: 0;
335
+ transform: translateX(-15px);
336
+ }
337
+ 100% {
338
+ opacity: 1;
339
+ transform: translateX(0);
340
+ }
341
+ }
342
+
343
+ /* Adding transition effects for interfaces */
344
+ @keyframes fadeInUp {
345
+ from {
346
+ opacity: 0;
347
+ transform: translateY(20px);
348
+ }
349
+ to {
350
+ opacity: 1;
351
+ transform: translateY(0);
352
+ }
353
+ }
354
+
355
+ @keyframes fadeInRight {
356
+ from {
357
+ opacity: 0;
358
+ transform: translateX(20px);
359
+ }
360
+ to {
361
+ opacity: 1;
362
+ transform: translateX(0);
363
+ }
364
+ }
365
+
366
+ @keyframes fadeInLeft {
367
+ from {
368
+ opacity: 0;
369
+ transform: translateX(-20px);
370
+ }
371
+ to {
372
+ opacity: 1;
373
+ transform: translateX(0);
374
+ }
375
+ }
376
+
377
+ /* Container animation classes that can be dynamically applied */
378
+ .animate-fadeInUp {
379
+ animation: fadeInUp 0.7s forwards;
380
+ }
381
+
382
+ .animate-fadeInRight {
383
+ animation: fadeInRight 0.7s forwards;
384
+ }
385
+
386
+ .animate-fadeInLeft {
387
+ animation: fadeInLeft 0.7s forwards;
388
+ }
389
+
390
+ /* Initial state for containers to be animated */
391
+ .pre-animation {
392
+ opacity: 0;
393
+ }
394
+
395
+ /* Specifically target Gradio elements to make them more mobile-friendly */
396
+ .gradio-container {
397
+ margin-left: 0 !important;
398
+ margin-right: 0 !important;
399
+ }
400
+
401
+ /* Adjust textbox and button widths for mobile */
402
+ .gr-textbox {
403
+ width: 100% !important;
404
+ }
405
+
406
+ .gr-button-primary {
407
+ width: 100% !important;
408
+ margin: 5px 0 !important;
409
+ }
410
+
411
+ /* Make all interface elements take more width on mobile */
412
+ .interface-elements {
413
+ width: 98% !important;
414
+ max-width: 100% !important;
415
+ margin: 0 auto !important;
416
+ }
417
+
418
+ /* Fix padding for form elements */
419
+ input, select, textarea, button {
420
+ box-sizing: border-box !important;
421
+ }
422
+
423
+ /* Ensure accordion, file inputs, and chat buttons are not affected by button min-height */
424
+ .accordion-button,
425
+ .file-upload button,
426
+ .chat-buttons button,
427
+ button[aria-label="Clear"],
428
+ button[aria-label="Retry"],
429
+ button[aria-label="Undo"] {
430
+ min-height: auto !important;
431
+ }
432
+
433
+ /* Media Queries for better responsiveness */
434
+ @media screen and (max-width: 768px) {
435
+ body {
436
+ padding: 0 !important; /* Remove body padding on mobile */
437
+ }
438
+
439
+ .dashboard-card {
440
+ width: 100% !important; /* Full width */
441
+ padding: 15px 8px !important; /* Reduced padding */
442
+ margin: 10px auto !important; /* Reduced margin */
443
+ border-radius: 0 !important; /* Optional: Remove border radius for full-width look */
444
+ }
445
+
446
+ .login-form-container {
447
+ width: 100% !important; /* Full width */
448
+ padding: 15px 10px !important; /* Reduced padding */
449
+ margin: 10px auto !important; /* Reduced margin */
450
+ border-radius: 0 !important; /* Optional: Remove border radius for full-width look */
451
+ }
452
+
453
+ .menu-button {
454
+ width: 98% !important; /* Almost full width */
455
+ font-size: 14px !important;
456
+ min-height: 50px !important;
457
+ }
458
+
459
+ .logo-dashboard {
460
+ max-height: 40px;
461
+ top: 10px;
462
+ left: 10px;
463
+ }
464
+
465
+ h1 {
466
+ font-size: 1.5rem !important;
467
+ margin-top: 10px !important;
468
+ margin-bottom: 15px !important;
469
+ }
470
+
471
+ h2 {
472
+ font-size: 1.3rem !important;
473
+ margin-top: 8px !important;
474
+ margin-bottom: 12px !important;
475
+ }
476
+
477
+ h3 {
478
+ font-size: 1.1rem !important;
479
+ margin-top: 6px !important;
480
+ margin-bottom: 10px !important;
481
+ }
482
+
483
+ /* Make chat interface take full width */
484
+ .chat-container, .chatbot-container {
485
+ width: 100% !important;
486
+ margin: 0 !important;
487
+ padding: 0 5px !important;
488
+ }
489
+
490
+ /* Adjust Gradio containers */
491
+ .gradio-container {
492
+ padding: 0 !important;
493
+ }
494
+ }
495
+
496
+ @media screen and (max-width: 480px) {
497
+ .dashboard-card {
498
+ padding: 10px 5px !important; /* Further reduced padding */
499
+ margin: 0 auto !important; /* Remove margin completely */
500
+ border-radius: 0 !important; /* Remove border radius */
501
+ width: 100% !important; /* Full width */
502
+ }
503
+
504
+ .login-form-container {
505
+ padding: 10px 5px !important; /* Further reduced padding */
506
+ margin: 0 auto !important; /* Remove margin completely */
507
+ border-radius: 0 !important; /* Remove border radius */
508
+ width: 100% !important; /* Full width */
509
+ }
510
+
511
+ .menu-button {
512
+ font-size: 13px !important;
513
+ min-height: 45px !important;
514
+ margin: 5px auto !important;
515
+ width: 100% !important; /* Full width */
516
+ border-radius: 6px !important; /* Slightly reduced border radius */
517
+ }
518
+
519
+ .logo-dashboard {
520
+ max-height: 35px;
521
+ top: 5px;
522
+ left: 5px;
523
+ }
524
+
525
+ /* Adjust spacing in forms for mobile */
526
+ input, select, textarea {
527
+ margin-bottom: 8px !important;
528
+ padding: 8px !important;
529
+ width: 100% !important;
530
+ }
531
+
532
+ /* Reduce padding and margins for all content */
533
+ .floating-card, .gr-box, .gr-panel {
534
+ padding: 10px 5px !important;
535
+ margin: 5px 0 !important;
536
+ width: 100% !important;
537
+ }
538
+
539
+ /* Full width for all UI components */
540
+ .gr-form, .gr-input, .gr-button, .gr-box {
541
+ width: 100% !important;
542
+ margin-left: 0 !important;
543
+ margin-right: 0 !important;
544
+ }
545
+ }
546
+
547
+ /* Additional responsive handling for extreme screen sizes */
548
+ @media screen and (max-width: 320px) {
549
+ /* For very small screens like older iPhones */
550
+ .menu-button {
551
+ font-size: 11px !important;
552
+ min-height: 40px !important;
553
+ padding: 8px 5px !important;
554
+ }
555
+
556
+ .dashboard-card {
557
+ padding: 5px 3px !important;
558
+ }
559
+
560
+ h1 {
561
+ font-size: 1.3rem !important;
562
+ }
563
+
564
+ h2 {
565
+ font-size: 1.1rem !important;
566
+ }
567
+
568
+ h3 {
569
+ font-size: 1rem !important;
570
+ }
571
+ }
572
+
573
+ @media screen and (min-width: 1600px) {
574
+ /* For very large screens */
575
+ .dashboard-card {
576
+ max-width: 1000px !important;
577
+ }
578
+
579
+ .menu-button {
580
+ max-width: 400px !important;
581
+ }
582
+ }
583
+
584
+ /* For landscape orientation on mobile */
585
+ @media screen and (max-height: 500px) and (orientation: landscape) {
586
+ .dashboard-menu {
587
+ min-height: auto;
588
+ padding: 5px 0;
589
+ }
590
+
591
+ .dashboard-card {
592
+ min-height: 300px !important;
593
+ }
594
+
595
+ .menu-button {
596
+ min-height: 40px !important;
597
+ margin: 4px auto !important;
598
+ }
599
+ }
600
+
601
+ /* Fixed positions for mobile views */
602
+ .fixed-top {
603
+ position: fixed;
604
+ top: 0;
605
+ left: 0;
606
+ right: 0;
607
+ z-index: 1000;
608
+ }
609
+
610
+ .fixed-bottom {
611
+ position: fixed;
612
+ bottom: 0;
613
+ left: 0;
614
+ right: 0;
615
+ z-index: 1000;
616
+ }
617
+
618
+ /* Ensure minimum touch target size for all clickable elements
619
+ BUT exclude accordion, file input, and chat action buttons */
620
+ button:not(.accordion-button):not(.file-upload button):not([aria-label="Clear"]):not([aria-label="Retry"]):not([aria-label="Undo"]):not(.chat-buttons button),
621
+ .gr-button:not(.file-upload .gr-button):not(.chat-buttons .gr-button),
622
+ a,
623
+ input[type="submit"],
624
+ input[type="button"] {
625
+ min-height: 44px !important; /* Apple's recommended minimum touch target size */
626
+ min-width: 44px !important;
627
+ }
628
+
629
+ /* Remove any unnecessary margins from Gradio components */
630
+ .gradio-container .gr-form > *, .gradio-container .gr-group > * {
631
+ margin-bottom: 8px !important;
632
+ }
633
+
634
+ /* Ensure Gradio container does not exceed viewport width */
635
+ #component-0, #component-1, #component-2, #component-3, #component-4, #component-5,
636
+ #component-6, #component-7, #component-8, #component-9, #component-10 {
637
+ max-width: 100vw !important;
638
+ overflow-x: hidden !important;
639
+ }
640
+ """
641
+ return css
642
+
643
+ # ----------------- Login Form UI -----------------
644
+
645
+ def login_form_ui():
646
+ with gr.Blocks() as login_form:
647
+
648
+ # USIU logo
649
+ gr.HTML('''
650
+ <div class="logo-container">
651
+ <img src="week5/chatbot_prototype/images/usiu-logo.png" alt="USIU Logo" class="logo" />
652
+ </div>
653
+ ''')
654
+
655
+ gr.Markdown("## Please Login")
656
+ user_id_input = gr.Textbox(
657
+ placeholder="Enter your User ID",
658
+ label="User ID",
659
+ type="email",
660
+ autofocus=True
661
+ )
662
+ # Error message for invalid user ID
663
+ user_id_error = gr.Markdown("", visible=True)
664
+
665
+ password_input = gr.Textbox(
666
+ placeholder="Enter your Password",
667
+ label="Password",
668
+ type="password"
669
+ )
670
+ # Error message for invalid password
671
+ password_error = gr.Markdown("", visible=True)
672
+
673
+ login_btn = gr.Button("Login")
674
+ # Error message for invalid credentials
675
+ login_msg = gr.Markdown("")
676
+ proceed_btn = gr.Button("Proceed", visible=False)
677
+ back_btn = gr.Button("Back to Dashboard")
678
+ # Return the login form along with the necessary components in order.
679
+ return login_form, user_id_input, user_id_error, password_input, password_error, login_btn, login_msg, proceed_btn, back_btn
680
+
681
+ # ----------------- Authentication Logic -----------------
682
+
683
+ def show_success(message: str, duration: float = 10, visible: bool = True, title: str = "Success") -> None:
684
+ # Bright, material-style success colors:
685
+ styled_message = (
686
+ f'<span style="color: #2e7d32; background-color: #c8e6c9; padding: 10px; '
687
+ f'border-radius: 4px; display: block;">{message}</span>'
688
+ )
689
+ gr.Success(styled_message, duration=duration, visible=visible, title=title)
690
+ # Add a small delay to help the modal appear
691
+ time.sleep(0.1)
692
+
693
+ def show_error(message: str, duration: float = 13, visible: bool = True, title: str = "Error") -> None:
694
+ # Bright, material-style error colors:
695
+ styled_message = (
696
+ f'<span style="color: #c62828; background-color: #ffcdd2; padding: 10px; '
697
+ f'border-radius: 4px; display: block;">{message}</span>'
698
+ )
699
+ gr.Error(styled_message, duration=duration, visible=visible, title=title)
700
+ # Add a small delay to help the modal appear
701
+ time.sleep(0.1)
702
+
703
+ def handle_login(user_id, password):
704
+ """
705
+ Validates login fields and the User ID (which must end with '@usiu.ac.ke').
706
+ - If both fields are empty, a generic error modal is shown.
707
+ - If a specific field is empty, an error message for that field is returned.
708
+ - On successful authentication, a success modal pops up.
709
+
710
+ Returns a tuple of five outputs:
711
+ 1. Overall status message (displayed below the login button).
712
+ 2. User ID field-specific error (displayed below the User ID input).
713
+ 3. Password field-specific error (displayed below the Password input).
714
+ 4. A gr.update() call to control the visibility of the "Proceed" button.
715
+ 5. A gr.update() call to control the visibility of the "Login" button.
716
+ """
717
+ try:
718
+ user_id = user_id.strip() if user_id else ""
719
+ password = password.strip() if password else ""
720
+
721
+ # Both fields empty:
722
+ if user_id == "" and password == "":
723
+ show_error("Please enter your User ID and Password.")
724
+ return (
725
+ "<span style='color:#c62828;'>Please enter your User ID and Password.</span>",
726
+ "", # No field-specific error for User ID.
727
+ "", # No field-specific error for Password.
728
+ gr.update(visible=False),
729
+ gr.update(visible=True)
730
+ )
731
+ # User ID is empty:
732
+ elif user_id == "":
733
+ show_error("User ID is required.")
734
+ return (
735
+ "",
736
+ "<span style='color:#c62828;'>User ID is required.</span>",
737
+ "",
738
+ gr.update(visible=False),
739
+ gr.update(visible=True)
740
+ )
741
+ # Password is empty:
742
+ elif password == "":
743
+ show_error("Password is required.")
744
+ return (
745
+ "",
746
+ "",
747
+ "<span style='color:#c62828;'>Password is required.</span>",
748
+ gr.update(visible=False),
749
+ gr.update(visible=True)
750
+ )
751
+ else:
752
+ # Validate the User ID: must end with "@usiu.ac.ke"
753
+ email_regex = r'^[\w\.-]+@usiu\.ac\.ke$'
754
+ if not re.match(email_regex, user_id):
755
+ show_error("User ID must be a valid \"@usiu.ac.ke\" email address.")
756
+ return (
757
+ "",
758
+ "<span style='color:#c62828;'>User ID must be a valid @usiu.ac.ke email address.</span>",
759
+ "",
760
+ gr.update(visible=False),
761
+ gr.update(visible=True)
762
+ )
763
+ # Attempt authentication.
764
+ if authenticate(user_id, password):
765
+ show_success("Login successful! Click 'Proceed' to continue!", duration=10)
766
+ return (
767
+ "",
768
+ "",
769
+ "",
770
+ gr.update(visible=True), # Show Proceed button.
771
+ gr.update(visible=False) # Hide Login button.
772
+ )
773
+ else:
774
+ show_error("Invalid credentials. Please try again.")
775
+ return (
776
+ "<span style='color:#c62828;'>Invalid credentials. Please try again.</span>",
777
+ "",
778
+ "",
779
+ gr.update(visible=False),
780
+ gr.update(visible=True)
781
+ )
782
+ except NameError as e:
783
+ show_error(f"System error: {str(e)}. Please ensure required modules are imported.")
784
+ return (
785
+ "<span style='color:#c62828;'>System error encountered.</span>",
786
+ "",
787
+ "",
788
+ gr.update(visible=False),
789
+ gr.update(visible=True)
790
+ )
791
+
792
+ # ---------- Dashboard UI ----------
793
+ def dashboard_ui(general_chat_prompt: str, general_model: str, study_prompt: str, study_model: str):
794
+ """
795
+ Creates a dashboard with professional styling, two main menus (General Chat and Study Support),
796
+ and smooth transitions between views. For Study Support, a login form (with authentication)
797
+ is shown before revealing the chat interface.
798
+ """
799
+ # Retrieve the existing chat interfaces.
800
+ general_chat_component = general_chat_ui(general_chat_prompt, general_model)
801
+ study_support_component = study_support_ui(study_prompt, study_model)
802
+ # Retrieve the login form.
803
+ (login_form_component, user_id_input, user_id_error, password_input, password_error,
804
+ login_btn, login_msg, proceed_btn, login_back_btn) = login_form_ui()
805
+
806
+ with gr.Blocks(css=custom_css()) as dashboard:
807
+ # Inject JavaScript for animations and loading effects
808
+ gr.HTML('''
809
+ <script>
810
+ document.addEventListener("DOMContentLoaded", function() {
811
+ // Create and style the spinner element
812
+ let spinner = document.createElement("div");
813
+ spinner.className = "loader";
814
+ spinner.innerHTML = '<div class="cube"></div>';
815
+ document.body.appendChild(spinner);
816
+
817
+ // Show the spinner when needed
818
+ spinner.style.display = "none"; // Initially hidden
819
+
820
+ // Function to show spinner
821
+ function showSpinner() {
822
+ spinner.style.display = "block";
823
+ }
824
+
825
+ // Function to hide spinner
826
+ function hideSpinner() {
827
+ spinner.style.display = "none";
828
+ }
829
+
830
+ // Function to handle animation transitions
831
+ function setupAnimations() {
832
+ // Get all containers that might be animated
833
+ const containers = document.querySelectorAll('.container, .chat-container');
834
+
835
+ // Add transition end listener to each container
836
+ containers.forEach(container => {
837
+ container.addEventListener('animationend', function(e) {
838
+ // When animation completes, remove the animation class
839
+ if (e.animationName.includes('fadeIn')) {
840
+ this.classList.remove('animate-fadeInUp', 'animate-fadeInRight', 'animate-fadeInLeft');
841
+ }
842
+ });
843
+ });
844
+ }
845
+
846
+ // Setup animations after a short delay to ensure DOM is ready
847
+ setTimeout(setupAnimations, 2000);
848
+
849
+ // Add event listeners to all buttons that should trigger the spinner
850
+ setTimeout(function(){
851
+ // Login and Proceed buttons with spinner
852
+ const loginBtn = document.querySelector('button:contains("Login")');
853
+ const proceedBtn = document.querySelector('button:contains("Proceed")');
854
+
855
+ if (loginBtn) {
856
+ loginBtn.addEventListener("click", function() {
857
+ showSpinner(); // Show spinner when login button is clicked
858
+
859
+ if (!this.classList.contains("loading")) {
860
+ const originalText = this.textContent;
861
+ this.setAttribute("data-original-text", originalText);
862
+ this.textContent = "";
863
+ this.classList.add("loading");
864
+
865
+ // Create spinner inside button
866
+ const btnSpinner = document.createElement("div");
867
+ btnSpinner.className = "btn-spinner";
868
+ btnSpinner.style.width = "20px";
869
+ btnSpinner.style.height = "20px";
870
+ btnSpinner.style.border = "3px solid rgba(255,255,255,0.3)";
871
+ btnSpinner.style.borderRadius = "50%";
872
+ btnSpinner.style.borderTopColor = "#fff";
873
+ btnSpinner.style.animation = "spin 1s linear infinite";
874
+ btnSpinner.style.display = "inline-block";
875
+ btnSpinner.style.verticalAlign = "middle";
876
+ this.appendChild(btnSpinner);
877
+
878
+ // Reset button after 3 seconds if no response
879
+ setTimeout(() => {
880
+ if (this.classList.contains("loading")) {
881
+ this.classList.remove("loading");
882
+ this.textContent = this.getAttribute("data-original-text");
883
+ }
884
+ hideSpinner(); // Hide spinner after timeout
885
+ }, 3000);
886
+ }
887
+ });
888
+ }
889
+
890
+ if (proceedBtn) {
891
+ proceedBtn.addEventListener("click", function() {
892
+ showSpinner(); // Show spinner when proceed button is clicked
893
+
894
+ if (!this.classList.contains("loading")) {
895
+ const originalText = this.textContent;
896
+ this.setAttribute("data-original-text", originalText);
897
+ this.textContent = "";
898
+ this.classList.add("loading");
899
+
900
+ // Create spinner inside button
901
+ const btnSpinner = document.createElement("div");
902
+ btnSpinner.className = "btn-spinner";
903
+ btnSpinner.style.width = "20px";
904
+ btnSpinner.style.height = "20px";
905
+ btnSpinner.style.border = "3px solid rgba(255,255,255,0.3)";
906
+ btnSpinner.style.borderRadius = "50%";
907
+ btnSpinner.style.borderTopColor = "#fff";
908
+ btnSpinner.style.animation = "spin 1s linear infinite";
909
+ btnSpinner.style.display = "inline-block";
910
+ btnSpinner.style.verticalAlign = "middle";
911
+ this.appendChild(btnSpinner);
912
+
913
+ // Reset button after 3 seconds if no response
914
+ setTimeout(() => {
915
+ if (this.classList.contains("loading")) {
916
+ this.classList.remove("loading");
917
+ this.textContent = this.getAttribute("data-original-text");
918
+ }
919
+ hideSpinner(); // Hide spinner after timeout
920
+ }, 3000);
921
+ }
922
+ });
923
+ }
924
+
925
+ // Scale effect for dashboard buttons
926
+ const dashboardButtons = document.querySelectorAll('.menu-button, button:contains("Back to Dashboard")');
927
+ dashboardButtons.forEach(function(btn) {
928
+ btn.addEventListener("click", function() {
929
+ this.style.transform = "scale(1.1)";
930
+ setTimeout(() => {
931
+ this.style.transform = "";
932
+ }, 200);
933
+ });
934
+ });
935
+ }, 2000);
936
+
937
+ // Add keyframes for spinner animation
938
+ const style = document.createElement('style');
939
+ style.textContent = `
940
+ @keyframes spin {
941
+ 0% { transform: rotate(0deg); }
942
+ 100% { transform: rotate(360deg); }
943
+ }
944
+ `;
945
+ document.head.appendChild(style);
946
+ });
947
+ </script>
948
+ ''')
949
+
950
+ # Logo for all pages - positioned in top-left corner
951
+ gr.HTML('''
952
+ <div class="logo-dashboard">
953
+ <!-- USIU Logo Placeholder - Replace the src with actual logo path -->
954
+ <img src="week5/chatbot_prototype/images/usiu-logo.png" alt="USIU Logo" class="logo" />
955
+ </div>
956
+ ''')
957
+
958
+ # Define containers for different views with initial animation state
959
+ dashboard_container = gr.Column(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInUp")
960
+ general_container = gr.Column(visible=False, elem_classes="chat-container pre-animation")
961
+ login_container = gr.Column(visible=False, elem_classes="container floating-card login-form-container pre-animation")
962
+ study_container = gr.Column(visible=False, elem_classes="chat-container pre-animation")
963
+
964
+ # ------------- Dashboard View -------------
965
+ with dashboard_container:
966
+ gr.Markdown("<h1 class='with-logo'>AI Assistance Dashboard</h1>")
967
+ with gr.Column(elem_classes="dashboard-menu"):
968
+ gen_button = gr.Button("General Enquiries", elem_classes="menu-button")
969
+ study_button = gr.Button("Study with AI", elem_classes="menu-button")
970
+
971
+ # ------------- General Academic Chat View -------------
972
+ with general_container:
973
+ back_gen = gr.Button("Back to Dashboard")
974
+ general_chat_component.render()
975
+
976
+ # ------------- Login Form (for Study Support) View -------------
977
+ with login_container:
978
+ login_form_component.render()
979
+
980
+ # ------------- Study Support Chat View -------------
981
+ with study_container:
982
+ back_study = gr.Button("Back to Dashboard")
983
+ study_support_component.render()
984
+
985
+ # ------------- Navigation Callbacks -------------
986
+ # When "General Academic Chat" is selected from the dashboard.
987
+ gen_button.click(
988
+ lambda: [
989
+ gr.update(visible=False),
990
+ gr.update(visible=True, elem_classes="chat-container animate-fadeInRight"),
991
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
992
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
993
+ ],
994
+ outputs=[dashboard_container, general_container, login_container, study_container]
995
+ )
996
+
997
+ # When "Study Support Chat" is selected from the dashboard, show the login view with animation.
998
+ study_button.click(
999
+ lambda: [
1000
+ gr.update(visible=False),
1001
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1002
+ gr.update(visible=True, elem_classes="container floating-card login-form-container animate-fadeInRight"),
1003
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
1004
+ ],
1005
+ outputs=[dashboard_container, general_container, login_container, study_container]
1006
+ )
1007
+
1008
+ # "Back to Dashboard" buttons with animation.
1009
+ back_gen.click(
1010
+ lambda: [
1011
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
1012
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1013
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1014
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
1015
+ ],
1016
+ outputs=[dashboard_container, general_container, login_container, study_container]
1017
+ )
1018
+
1019
+ back_study.click(
1020
+ lambda: [
1021
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
1022
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1023
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1024
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
1025
+ ],
1026
+ outputs=[dashboard_container, general_container, login_container, study_container]
1027
+ )
1028
+
1029
+ login_back_btn.click(
1030
+ lambda: [
1031
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
1032
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1033
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1034
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
1035
+ ],
1036
+ outputs=[dashboard_container, general_container, login_container, study_container]
1037
+ )
1038
+
1039
+ # ------------- Authentication Logic -------------
1040
+ login_btn.click(
1041
+ handle_login,
1042
+ inputs=[user_id_input, password_input],
1043
+ outputs=[login_msg, user_id_error, password_error, proceed_btn, login_btn]
1044
+ )
1045
+
1046
+ # When "Proceed" is clicked (after successful authentication), show the study support chat with animation.
1047
+ proceed_btn.click(
1048
+ lambda: [
1049
+ gr.update(visible=False),
1050
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1051
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1052
+ gr.update(visible=True, elem_classes="chat-container animate-fadeInRight")
1053
+ ],
1054
+ outputs=[dashboard_container, general_container, login_container, study_container]
1055
+ )
1056
+
1057
+ return dashboard.queue()
1058
+
1059
+ import speech_recognition as sr
1060
+ from gtts import gTTS
1061
+ import os
1062
+ import tempfile
1063
+ import time
1064
+ import threading
1065
+ import queue
1066
+ from playsound import playsound
1067
+ # from voice_interface import VoiceInterface
1068
+
1069
+ class VoiceInterface:
1070
+ def __init__(self):
1071
+ # Initialize speech recognizer
1072
+ self.recognizer = sr.Recognizer()
1073
+ self.audio_queue = queue.Queue()
1074
+ self.recording = False
1075
+ self.temp_dir = tempfile.mkdtemp()
1076
+
1077
+ def start_recording(self):
1078
+ """Start recording audio from the microphone"""
1079
+ self.recording = True
1080
+ threading.Thread(target=self._record_audio, daemon=True).start()
1081
+ print("Recording started... Speak now!")
1082
+
1083
+ def stop_recording(self):
1084
+ """Stop recording audio"""
1085
+ self.recording = False
1086
+ print("Recording stopped.")
1087
+
1088
+ def _record_audio(self):
1089
+ """Record audio from microphone"""
1090
+ try:
1091
+ with sr.Microphone() as source:
1092
+ # Adjust for ambient noise
1093
+ self.recognizer.adjust_for_ambient_noise(source, duration=0.5)
1094
+
1095
+ while self.recording:
1096
+ try:
1097
+ print("Listening...")
1098
+ audio = self.recognizer.listen(source, timeout=5, phrase_time_limit=10)
1099
+
1100
+ # Process the audio in a separate thread
1101
+ threading.Thread(
1102
+ target=self._process_audio,
1103
+ args=(audio,),
1104
+ daemon=True
1105
+ ).start()
1106
+ except sr.WaitTimeoutError:
1107
+ continue
1108
+ except Exception as e:
1109
+ print(f"Error while listening: {e}")
1110
+ time.sleep(1)
1111
+
1112
+ except Exception as e:
1113
+ print(f"Error in recording thread: {e}")
1114
+
1115
+ def _process_audio(self, audio):
1116
+ """Process recorded audio and convert to text"""
1117
+ try:
1118
+ # Use multiple APIs for better reliability
1119
+ text = None
1120
+ apis = [
1121
+ lambda: self.recognizer.recognize_google(audio),
1122
+ lambda: self.recognizer.recognize_whisper(audio),
1123
+ lambda: self.recognizer.recognize_sphinx(audio)
1124
+ ]
1125
+
1126
+ for api_func in apis:
1127
+ try:
1128
+ text = api_func()
1129
+ break
1130
+ except:
1131
+ continue
1132
+
1133
+ if text and text.strip():
1134
+ print(f"Recognized: {text}")
1135
+ self.audio_queue.put(text)
1136
+ else:
1137
+ print("Speech not recognized")
1138
+
1139
+ except Exception as e:
1140
+ print(f"Error processing speech: {e}")
1141
+
1142
+ def get_text_from_speech(self, timeout=None):
1143
+ """Get recognized text from the audio queue"""
1144
+ try:
1145
+ return self.audio_queue.get(timeout=timeout)
1146
+ except queue.Empty:
1147
+ return None
1148
+
1149
+ def text_to_speech(self, text):
1150
+ """Convert text to speech and play it"""
1151
+ try:
1152
+ # Create temporary file for audio
1153
+ temp_file = os.path.join(self.temp_dir, f"speech_{int(time.time())}.mp3")
1154
+
1155
+ # Generate speech
1156
+ tts = gTTS(text=text, lang='en', slow=False)
1157
+ tts.save(temp_file)
1158
+
1159
+ # Play the audio
1160
+ playsound(temp_file)
1161
+
1162
+ # Clean up file after playing
1163
+ if os.path.exists(temp_file):
1164
+ os.remove(temp_file)
1165
+
1166
+ except Exception as e:
1167
+ print(f"Error in text-to-speech: {e}")
1168
+
1169
+ def cleanup(self):
1170
+ """Clean up temporary files"""
1171
+ try:
1172
+ for file in os.listdir(self.temp_dir):
1173
+ os.remove(os.path.join(self.temp_dir, file))
1174
+ os.rmdir(self.temp_dir)
1175
+ except:
1176
+ pass
1177
+
1178
+ #############################
1179
+ # READ FILE UTILITY
1180
+ #############################
1181
+ def read_file(file):
1182
+ file_extension = os.path.splitext(file.name)[1].lower()
1183
+
1184
+ if file_extension == '.txt':
1185
+ content = file.read().decode('utf-8')
1186
+ elif file_extension == '.pdf':
1187
+ pdf_reader = PyPDF2.PdfReader(file)
1188
+ content = ''
1189
+ for page in pdf_reader.pages:
1190
+ text = page.extract_text()
1191
+ if text:
1192
+ content += text
1193
+ elif file_extension in ['.docx', '.doc']:
1194
+ doc = docx.Document(file)
1195
+ content = "\n".join([para.text for para in doc.paragraphs])
1196
+ elif file_extension in ['.xlsx', '.xls']:
1197
+ df = pd.read_excel(file)
1198
+ content = df.to_string()
1199
+ elif file_extension in ['.pptx', '.ppt']:
1200
+ prs = pptx.Presentation(file)
1201
+ content = ''
1202
+ for slide in prs.slides:
1203
+ for shape in slide.shapes:
1204
+ if hasattr(shape, 'text'):
1205
+ content += shape.text + '\n'
1206
+ elif file_extension in ['.png', '.jpg', '.jpeg', '.gif']:
1207
+ image = Image.open(file)
1208
+ buffered = io.BytesIO()
1209
+ image.save(buffered, format="PNG")
1210
+ # Create an image variation via OpenAI's API (example)
1211
+ response = openai.Image.create_variation(
1212
+ image=buffered,
1213
+ n=1,
1214
+ size="256x256"
1215
+ )
1216
+ image_description = response['data'][0]['url']
1217
+ content = f"[Image uploaded. Description: {image_description}]"
1218
+ else:
1219
+ content = f"Unsupported file type: {file_extension}"
1220
+
1221
+ return content
1222
+
1223
+ #############################
1224
+ # GLOBAL RETRIEVER SETUP (for RAG in general_chat_ui)
1225
+ #############################
1226
+ from vector_services.data_curator import DataCurator
1227
+ curator = DataCurator(knowledge_base_dir="usiu-knowledge-base")
1228
+ curator.load_vectorstore()
1229
+ # GLOBAL_RETRIEVER enables RAG for general chat.
1230
+ GLOBAL_RETRIEVER = curator.get_retriever()
1231
+
1232
+ #############################
1233
+ # GENERAL CHAT UI (with RAG)
1234
+ #############################
1235
+ def general_chat_ui(system_prompt: str, model: str) -> None:
1236
+ """
1237
+ Creates a Gradio ChatInterface for general academic chat.
1238
+ Uses additional_inputs for file upload, maintains RAG by retrieving context via GLOBAL_RETRIEVER,
1239
+ and streams the response. The function takes three inputs (text message, state, file) and
1240
+ yields incremental final responses as a string.
1241
+ """
1242
+
1243
+ # def voice_button_click():
1244
+ # """Callback for the voice input button"""
1245
+ # voice_interface.start_recording()
1246
+ # # Wait for up to 10 seconds for voice input
1247
+ # text = voice_interface.get_text_from_speech(timeout=10)
1248
+ # voice_interface.stop_recording()
1249
+ # return text if text else ""
1250
+
1251
+ def general_chat(message: str, chat_history, file) -> str:
1252
+ # Ensure chat_history is a list.
1253
+ if chat_history is None:
1254
+ chat_history = []
1255
+
1256
+ # If a file is uploaded and the message is empty, replace the empty message with the file name.
1257
+ if not message.strip() and file is not None:
1258
+ message = f"Uploaded file: {os.path.basename(file.name)}"
1259
+
1260
+ # If this is a new conversation, insert a conversation header.
1261
+ if not chat_history:
1262
+ date_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
1263
+ conv_header = {
1264
+ "role": "header",
1265
+ "content": (
1266
+ f"<div style='text-align: center; font-weight: bold; font-size: 16px; color: #000;'>"
1267
+ f"Conversation started on {date_str}"
1268
+ f"</div>"
1269
+ ),
1270
+ "files": []
1271
+ }
1272
+ chat_history.insert(0, conv_header) # Insert at the beginning
1273
+
1274
+ # Start with the initial system prompt.
1275
+ messages = [
1276
+ {"role": "system", "content": system_prompt, "files": []},
1277
+ ]
1278
+
1279
+ # Add chat history to messages (all messages, including header).
1280
+ if chat_history:
1281
+ # If the first element is a tuple/list with two elements, assume pair format.
1282
+ if isinstance(chat_history[0], (tuple, list)) and len(chat_history[0]) == 2:
1283
+ for human, assistant in chat_history:
1284
+ messages.append({"role": "user", "content": human, "files": []})
1285
+ messages.append({"role": "assistant", "content": assistant, "files": []})
1286
+ # Otherwise, if they are dicts, include all messages.
1287
+ elif isinstance(chat_history[0], dict):
1288
+ for msg in chat_history:
1289
+ if msg is None:
1290
+ continue
1291
+ # Skip header messages when sending to API.
1292
+ if msg.get("role") == "header":
1293
+ continue
1294
+ # Ensure each message has a files field.
1295
+ msg_to_send = dict(msg)
1296
+ if "files" not in msg_to_send or msg_to_send["files"] is None:
1297
+ msg_to_send["files"] = []
1298
+ messages.append(msg_to_send)
1299
+
1300
+ # If a file is uploaded, add its content to the messages.
1301
+ if file is not None:
1302
+ try:
1303
+ file_content = read_file(file)
1304
+ messages.append({
1305
+ "role": "user",
1306
+ "content": f"Here's the content of the uploaded file:\n\n{file_content}"
1307
+ })
1308
+ except Exception as e:
1309
+ messages.append({
1310
+ "role": "user",
1311
+ "content": f"Error reading file: {str(e)}",
1312
+ "files": []
1313
+ })
1314
+
1315
+ # RAG: Retrieve additional context using GLOBAL_RETRIEVER if available.
1316
+ if GLOBAL_RETRIEVER is not None:
1317
+ try:
1318
+ docs = GLOBAL_RETRIEVER.get_relevant_documents(message)
1319
+ if docs:
1320
+ context_text = "\n\n".join([doc.page_content for doc in docs])
1321
+ # Insert the retrieved context as an additional system message.
1322
+ messages.insert(1, {
1323
+ "role": "system",
1324
+ "content": f"Additional Context:\n{context_text}",
1325
+ "files": []
1326
+ })
1327
+ except Exception as e:
1328
+ print("RAG retrieval failed:", e)
1329
+
1330
+ chat_history.append({"role": "user", "content": message, "files": []})
1331
+ messages.append({"role": "user", "content": message, "files": []})
1332
+
1333
+ print("General Enquiries - Conversation with context:")
1334
+ print(messages)
1335
+
1336
+ # Call OpenAI's ChatCompletion with streaming enabled.
1337
+ completion = openai.ChatCompletion.create(
1338
+ model=model,
1339
+ messages=messages,
1340
+ max_tokens=2_000,
1341
+ temperature=0.7,
1342
+ stream=True,
1343
+ )
1344
+
1345
+ response = ""
1346
+ for chunk in completion:
1347
+ token = chunk.choices[0].delta.get("content", "")
1348
+ response += token
1349
+ yield response
1350
+
1351
+ # # After the response is completely generated, append a timestamp.
1352
+ # timestamp = time.strftime("%I:%M %p", time.localtime())
1353
+ # final_response = response + f"\n<span style='font-size:11px; color:#999;'>{timestamp}</span>"
1354
+
1355
+ # # If voice playback is enabled, read the response aloud in a separate thread
1356
+ # if play_response:
1357
+ # threading.Thread(
1358
+ # target=voice_interface.text_to_speech,
1359
+ # args=(response,),
1360
+ # daemon=True
1361
+ # ).start()
1362
+
1363
+ # yield final_response
1364
+
1365
+ # iface = gr.ChatInterface(
1366
+ # fn=general_chat,
1367
+ # additional_inputs=[gr.File(label="Upload a file (optional)")],
1368
+ # additional_inputs_accordion="File Upload",
1369
+ # editable=True,
1370
+ # save_history=True,
1371
+ # title="General Academic Chat with File Upload",
1372
+ # type="messages",
1373
+ # cache_mode="eager"
1374
+ # )
1375
+ iface = gr.ChatInterface(
1376
+ fn=general_chat,
1377
+ additional_inputs=[gr.File(label="Upload a file (optional)", elem_classes="file-upload")],
1378
+ additional_inputs_accordion=gr.Accordion("File Upload", open=False, elem_classes="additional-inputs-accordion"),
1379
+ editable=True,
1380
+ save_history=True,
1381
+ title="General Enquiries",
1382
+ description="<div style='display:flex; justify-content:center;'>Hi there! I'm your AI assistant, here to help you with queries related to USIU. Feel free to ask any questions or seek assistance.</div>",
1383
+ type="messages",
1384
+ cache_mode="eager"
1385
+ )
1386
+ return iface.queue()
1387
+
1388
+ #############################
1389
+ # STUDY SUPPORT UI (without RAG)
1390
+ #############################
1391
+ def study_support_ui(system_prompt: str, model: str) -> None:
1392
+ """
1393
+ Creates a Gradio ChatInterface for study support.
1394
+ Uses additional_inputs for file upload and streams the response using Claude's API.
1395
+ No RAG is applied in this interface.
1396
+ The function takes three inputs (text message, state, file) and returns a final response as a string.
1397
+ """
1398
+
1399
+ # def voice_button_click():
1400
+ # """Callback for the voice input button"""
1401
+ # voice_interface.start_recording()
1402
+ # # Wait for up to 10 seconds for voice input
1403
+ # text = voice_interface.get_text_from_speech(timeout=10)
1404
+ # voice_interface.stop_recording()
1405
+ # return text if text else ""
1406
+
1407
+ def study_support_chat(message: str, chat_history, file) -> str:
1408
+ # Ensure chat_history is a list.
1409
+ if chat_history is None:
1410
+ chat_history = []
1411
+
1412
+ # If a file is uploaded and the message is empty, replace the empty message with the file name.
1413
+ if not message.strip() and file is not None:
1414
+ message = f"Uploaded file: {os.path.basename(file.name)}"
1415
+
1416
+ # If this is a new conversation, insert a conversation header.
1417
+ if not chat_history:
1418
+ date_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
1419
+ conv_header = {
1420
+ "role": "header",
1421
+ "content": (
1422
+ f"<div style='text-align: center; font-weight: bold; font-size: 16px; color: #000;'>"
1423
+ f"Conversation started on {date_str}"
1424
+ f"</div>"
1425
+ )
1426
+ }
1427
+ chat_history.insert(0, conv_header)
1428
+
1429
+ # Build the messages list that will be sent to the API.
1430
+ messages = []
1431
+ if chat_history:
1432
+ # If chat_history items are tuples/lists with two elements, unpack them.
1433
+ if isinstance(chat_history[0], (tuple, list)) and len(chat_history[0]) == 2:
1434
+ for human, assistant in chat_history:
1435
+ messages.append({"role": "user", "content": human})
1436
+ messages.append({"role": "assistant", "content": assistant})
1437
+ # Otherwise, if they are dicts, include only "role" and "content" for non-system messages.
1438
+ elif isinstance(chat_history[0], dict):
1439
+ for msg in chat_history:
1440
+ if msg is None:
1441
+ continue
1442
+ if msg.get("role") == "header":
1443
+ continue # Skip header messages.
1444
+ msg_to_send = {"role": msg.get("role"), "content": msg.get("content")}
1445
+ messages.append(msg_to_send)
1446
+
1447
+ # Add the current message.
1448
+ messages.append({"role": "user", "content": message})
1449
+
1450
+ # If a file is uploaded, add its content.
1451
+ if file is not None:
1452
+ try:
1453
+ file_content = read_file(file)
1454
+ messages.append({
1455
+ "role": "user",
1456
+ "content": f"Here's the content of the uploaded file:\n\n{file_content}"
1457
+ })
1458
+ except Exception as e:
1459
+ messages.append({
1460
+ "role": "user",
1461
+ "content": f"Error reading file: {str(e)}"
1462
+ })
1463
+
1464
+ # Implement a retry mechanism to handle overloaded errors.
1465
+ max_retries = 7
1466
+ delay = 3 # seconds
1467
+ attempt = 0
1468
+ while attempt < max_retries:
1469
+ try:
1470
+ result = claude.messages.stream(
1471
+ model=model,
1472
+ max_tokens=64_000,
1473
+ system=system_prompt,
1474
+ messages=messages,
1475
+ extra_query={"extended_thinking": True}
1476
+ )
1477
+ response = ""
1478
+ with result as stream:
1479
+ for text in stream.text_stream:
1480
+ response += text
1481
+ yield response # Yield incremental responses.
1482
+
1483
+ # After generating the complete response
1484
+ # if play_response:
1485
+ # threading.Thread(
1486
+ # target=voice_interface.text_to_speech,
1487
+ # args=(response,),
1488
+ # daemon=True
1489
+ # ).start()
1490
+
1491
+ # yield final_response
1492
+
1493
+ break # Exit the retry loop if successful.
1494
+ except Exception as e:
1495
+ # Check if error indicates the API is overloaded.
1496
+ if "overloaded" in str(e).lower():
1497
+ attempt += 1
1498
+ yield f"Service unavailable. Retrying in {delay} seconds... (attempt {attempt}/{max_retries})\n"
1499
+ time.sleep(delay)
1500
+ else:
1501
+ print(f"An error occurred: {str(e)}")
1502
+ break
1503
+ else:
1504
+ yield "The service is currently unavailable. Please try again later."
1505
+
1506
+ # iface = gr.ChatInterface(
1507
+ # fn=study_support_chat,
1508
+ # additional_inputs=[gr.File(label="Upload a file (optional)")],
1509
+ # additional_inputs_accordion="File Upload",
1510
+ # editable=True,
1511
+ # save_history=True,
1512
+ # title="Study Support Chat with File Upload",
1513
+ # type="messages",
1514
+ # cache_mode="eager"
1515
+ # )
1516
+ iface = gr.ChatInterface(
1517
+ fn=study_support_chat,
1518
+ additional_inputs=[gr.File(label="Upload a file (optional)", elem_classes="file-upload")],
1519
+ additional_inputs_accordion=gr.Accordion("File Upload", open=False, elem_classes="additional-inputs-accordion"),
1520
+ editable=True,
1521
+ save_history=True,
1522
+ title="Study Bud",
1523
+ description="<div style='display:flex; justify-content:center;'>Hi there! I'm your AI study bud. Feel free to ask questions, share resources, and get personalized support.</div>",
1524
+ type="messages",
1525
+ cache_mode="eager"
1526
+ )
1527
+ return iface.queue()
services/ui_service.py ADDED
@@ -0,0 +1,1560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # services/ui_service.py
2
+
3
+ import re
4
+ import os
5
+ import time
6
+ import openai
7
+ import gradio as gr
8
+ import threading
9
+ from typing import List, Dict, Any, Tuple, Optional, Generator
10
+
11
+ from configs.config import claude, GPT4O_MODEL, CLAUDE_MODEL
12
+ from services.auth_service import (
13
+ authenticate, create_session, get_session, update_session_activity,
14
+ end_session, check_inactivity, INACTIVITY_TIMEOUT
15
+ )
16
+ from services.file_service import FileProcessor
17
+ from api_gateway.gateway import route_request
18
+ # from vector_services.data_curator import DataCurator
19
+
20
+ # Will be set when dashboard_ui is called (during runtime)
21
+ GLOBAL_RETRIEVER = None
22
+
23
+
24
+ # ----------------- New Summarization Helper -----------------
25
+ def summarize_context(text: str, summarization_model: str = GPT4O_MODEL) -> str:
26
+ """
27
+ Summarize the given text concisely while preserving key details.
28
+ Uses the provided summarization model (e.g., GPT-4o or llama-3-8b).
29
+ """
30
+ summarization_prompt = (
31
+ "Summarize the following text concisely while preserving the essential details:\n\n"
32
+ f"{text}\n\nSummary:"
33
+ )
34
+ try:
35
+ # Non-streaming call to get the summary
36
+ response = openai.ChatCompletion.create(
37
+ model=summarization_model,
38
+ messages=[{"role": "system", "content": summarization_prompt}],
39
+ max_tokens=150,
40
+ temperature=0.5,
41
+ )
42
+ summary = response.choices[0].message.content.strip()
43
+ return summary
44
+ except Exception as e:
45
+ print("Summarization failed:", e)
46
+ # Fallback: return a truncated version if summarization fails
47
+ return text[:1000]
48
+
49
+ def custom_css():
50
+ css = """
51
+ /* Professional custom CSS with transitions, hover effects, and responsive design */
52
+ body {
53
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
54
+ background-color: #f8f9fa;
55
+ margin: 0;
56
+ padding: 0;
57
+ width: 100%;
58
+ overflow-x: hidden; /* Prevent horizontal scrolling */
59
+ }
60
+ h1, h2, h3 {
61
+ text-align: center;
62
+ color: #495057;
63
+ }
64
+ .gr-button:not(.secondary-button):not([aria-label="Clear"]):not([aria-label="Retry"]):not([aria-label="Undo"]):not(.chat-buttons button) {
65
+ transition: background-color 0.3s ease, transform 0.3s ease;
66
+ border-radius: 4px;
67
+ /* USIU-Africa theme color for buttons */
68
+ background-color: #000080;
69
+ color: white;
70
+ }
71
+ .gr-button:not(.secondary-button):not([aria-label="Clear"]):not([aria-label="Retry"]):not([aria-label="Undo"]):not(.chat-buttons button):hover {
72
+ background-color: #000066;
73
+ color: #fff;
74
+ transform: scale(1.03);
75
+ }
76
+ .container {
77
+ transition: opacity 0.5s ease-in-out;
78
+ padding: 4px;
79
+ }
80
+
81
+ /* Style the "New chat" button to match USIU-Africa theme */
82
+ button.secondary-button {
83
+ background-color: #000080 !important; /* USIU-Africa navy blue */
84
+ color: white !important;
85
+ }
86
+
87
+ button.secondary-button:hover {
88
+ background-color: #000066 !important; /* Slightly darker on hover */
89
+ }
90
+
91
+ /* Glassmorphism effect for floating cards */
92
+ .floating-card {
93
+ background: rgba(255, 255, 255, 0.85);
94
+ border-radius: 12px;
95
+ box-shadow:
96
+ 0 10px 25px rgba(0,0,0,0.08),
97
+ 0 6px 10px rgba(0,0,0,0.12),
98
+ 0 3px 3px rgba(0,0,0,0.15);
99
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
100
+ padding: 25px;
101
+ margin: 15px auto; /* Reduced margin */
102
+ backdrop-filter: blur(10px);
103
+ border: 1px solid rgba(255,255,255,0.25);
104
+ width: 95%; /* Increased width */
105
+ max-width: 98%; /* Increased max-width */
106
+ }
107
+
108
+ .floating-card:hover {
109
+ box-shadow:
110
+ 0 15px 30px rgba(0,0,0,0.15),
111
+ 0 10px 10px rgba(0,0,0,0.18);
112
+ transform: translateY(-5px);
113
+ background: rgba(255, 255, 255, 0.9);
114
+ }
115
+
116
+ /* Login form styling with responsiveness */
117
+ .login-form-container {
118
+ width: 98% !important; /* Increased width for mobile */
119
+ max-width: 500px !important;
120
+ margin: 30px auto !important; /* Reduced top/bottom margin */
121
+ background: rgba(255, 255, 255, 0.88) !important;
122
+ backdrop-filter: blur(12px) !important;
123
+ border: 1px solid rgba(255, 255, 255, 0.3) !important;
124
+ border-top: 3px solid #000080 !important;
125
+ padding: 20px 15px !important; /* Added horizontal padding */
126
+ }
127
+
128
+ /* Dashboard container styling with responsiveness */
129
+ .dashboard-card {
130
+ width: 98% !important; /* Take almost full width on mobile */
131
+ max-width: 700px !important;
132
+ margin: 20px auto !important; /* Reduced margin */
133
+ background: rgba(255, 255, 255, 0.88) !important;
134
+ backdrop-filter: blur(12px) !important;
135
+ height: auto !important;
136
+ min-height: 500px !important;
137
+ border: 1px solid rgba(255, 255, 255, 0.3) !important;
138
+ border-top: 3px solid #000080 !important;
139
+ opacity: 0;
140
+ animation: dashboardFadeIn 1.5s forwards ease-in-out;
141
+ padding: 20px 10px !important; /* Reduced horizontal padding */
142
+ }
143
+
144
+ /* A subtle gradient background to the cards */
145
+ .dashboard-card, .login-form-container {
146
+ background: linear-gradient(
147
+ 135deg,
148
+ rgba(255, 255, 255, 0.9) 0%,
149
+ rgba(255, 255, 255, 0.8) 100%
150
+ ) !important;
151
+ }
152
+
153
+ /* Logo container styling with responsive adjustments */
154
+ .logo-container {
155
+ text-align: center;
156
+ margin-bottom: 25px;
157
+ position: relative;
158
+ width: 100%;
159
+ }
160
+
161
+ .logo {
162
+ max-height: 80px;
163
+ max-width: 90%; /* Increased max-width */
164
+ margin: 0 auto;
165
+ display: block;
166
+ }
167
+
168
+ .logo-dashboard {
169
+ position: absolute;
170
+ top: 15px;
171
+ left: 15px;
172
+ max-height: 60px;
173
+ max-width: 35%; /* Increased max-width */
174
+ z-index: 100;
175
+ opacity: 1; /* Changed from 0 to 1 to make it fully visible */
176
+ }
177
+
178
+ .with-logo {
179
+ margin-top: 10px;
180
+ }
181
+
182
+ /* Chat UI container with extended width and responsiveness */
183
+ .chat-container {
184
+ max-width: 1000px;
185
+ width: 98%; /* Increased width for mobile */
186
+ margin: 0 auto;
187
+ opacity: 0;
188
+ transform: translateY(20px);
189
+ animation: fadeInUp 0.7s forwards;
190
+ }
191
+
192
+ /* Increase chat window height with responsive adjustment */
193
+ .chatbot-container, .gradio-container .chat {
194
+ height: 80vh !important;
195
+ max-height: 900px !important;
196
+ width: 98% !important; /* Take up more width */
197
+ margin: 0 auto !important;
198
+ }
199
+
200
+ /* Make Gradio container take full width on mobile */
201
+ .gradio-container {
202
+ width: 100% !important;
203
+ max-width: 100% !important;
204
+ margin: 0 !important;
205
+ padding: 0 !important;
206
+ }
207
+
208
+ /* Logout button styling */
209
+ .logout-btn {
210
+ /* When using a Gradio Button with elem_classes, these styles will be applied */
211
+ background-color: #f44336 !important;
212
+ color: white !important;
213
+ border: none !important;
214
+ padding: 8px 16px !important;
215
+ cursor: pointer !important;
216
+ border-radius: 4px !important;
217
+ display: flex !important;
218
+ align-items: center !important;
219
+ gap: 8px !important;
220
+ font-size: 14px !important;
221
+ transition: background-color 0.3s ease !important;
222
+ }
223
+
224
+ .logout-btn:hover {
225
+ background-color: #d32f2f !important;
226
+ }
227
+
228
+ .logout-btn svg {
229
+ width: 16px;
230
+ height: 16px;
231
+ fill: white;
232
+ }
233
+
234
+ .logout-btn-container {
235
+ position: relative;
236
+ width: 100%;
237
+ height: 0px;
238
+ }
239
+
240
+ .session-info {
241
+ position: absolute;
242
+ top: 50px;
243
+ right: 15px;
244
+ font-size: 12px;
245
+ color: #666;
246
+ background-color: rgba(255, 255, 255, 0.7);
247
+ padding: 4px 8px;
248
+ border-radius: 4px;
249
+ }
250
+
251
+ /* Inactivity warning modal */
252
+ .inactive-warning {
253
+ position: fixed;
254
+ top: 50%;
255
+ left: 50%;
256
+ transform: translate(-50%, -50%);
257
+ background-color: white;
258
+ border: 1px solid #ccc;
259
+ border-radius: 8px;
260
+ padding: 20px;
261
+ z-index: 1001;
262
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
263
+ max-width: 400px;
264
+ width: 90%;
265
+ text-align: center;
266
+ display: none;
267
+ }
268
+
269
+ .inactive-warning h3 {
270
+ margin-top: 0;
271
+ color: #f44336;
272
+ }
273
+
274
+ .inactive-warning button {
275
+ margin-top: 15px;
276
+ background-color: #000080;
277
+ color: white;
278
+ border: none;
279
+ padding: 8px 16px;
280
+ border-radius: 4px;
281
+ cursor: pointer;
282
+ }
283
+
284
+ .inactive-warning button:hover {
285
+ background-color: #000066;
286
+ }
287
+
288
+ .overlay {
289
+ position: fixed;
290
+ top: 0;
291
+ left: 0;
292
+ right: 0;
293
+ bottom: 0;
294
+ background-color: rgba(0,0,0,0.5);
295
+ z-index: 1000;
296
+ display: none;
297
+ }
298
+
299
+ /* Dashboard menu buttons styling with responsiveness */
300
+ .dashboard-menu {
301
+ display: flex;
302
+ flex-direction: column;
303
+ align-items: center;
304
+ justify-content: center;
305
+ min-height: 60vh;
306
+ gap: 8px;
307
+ opacity: 0;
308
+ animation: menuFadeIn 3s forwards ease-in-out;
309
+ animation-delay: 0.7s;
310
+ padding: 15px 0; /* Reduced padding */
311
+ width: 100%; /* Full width */
312
+ }
313
+
314
+ .menu-button {
315
+ width: 95% !important; /* Increased width for mobile */
316
+ max-width: 300px !important;
317
+ height: auto !important;
318
+ min-height: 60px !important;
319
+ font-size: 16px !important;
320
+ font-weight: 500 !important;
321
+ margin: 6px auto !important; /* Reduced margin */
322
+ border-radius: 8px !important;
323
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
324
+ transition: transform 0.3s ease, box-shadow 0.3s ease !important;
325
+ background-color: #000080 !important;
326
+ color: white !important;
327
+ opacity: 0;
328
+ animation: buttonsFadeIn 0.7s forwards ease-in-out;
329
+ animation-delay: calc(var(--button-index, 0) * 0.3s + 1s);
330
+ padding: 10px 15px !important;
331
+ display: flex !important;
332
+ align-items: center !important;
333
+ justify-content: center !important;
334
+ text-align: center !important;
335
+ word-wrap: break-word !important;
336
+ }
337
+
338
+ .menu-button:hover {
339
+ transform: scale(1.02);
340
+ background-image: linear-gradient(to bottom right, #000080, #0000cc);
341
+ transition: transform 0.5s ease;
342
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important;
343
+ }
344
+
345
+ /* 3D Loader CSS with responsive sizing */
346
+ .loader {
347
+ width: 40px; /* Reduced size */
348
+ height: 40px; /* Reduced size */
349
+ perspective: 100px;
350
+ position: fixed;
351
+ top: 50%;
352
+ left: 50%;
353
+ transform: translate(-50%, -50%);
354
+ z-index: 9999;
355
+ display: none;
356
+ }
357
+ .cube {
358
+ width: 100%;
359
+ height: 100%;
360
+ background: #000080;
361
+ transform: rotateX(0deg) rotateY(0deg);
362
+ animation: spinCube 1.5s infinite ease-in-out;
363
+ }
364
+ @keyframes spinCube {
365
+ 0% { transform: rotateX(0deg) rotateY(0deg); }
366
+ 25% { transform: rotateX(90deg) rotateY(0deg); }
367
+ 50% { transform: rotateX(90deg) rotateY(90deg); }
368
+ 75% { transform: rotateX(0deg) rotateY(90deg); }
369
+ 100% { transform: rotateX(0deg) rotateY(0deg); }
370
+ }
371
+
372
+ /* Dashboard specific fade-in animations */
373
+ @keyframes dashboardFadeIn {
374
+ 0% {
375
+ opacity: 0;
376
+ transform: translateY(30px);
377
+ }
378
+ 100% {
379
+ opacity: 1;
380
+ transform: translateY(0);
381
+ }
382
+ }
383
+
384
+ @keyframes menuFadeIn {
385
+ 0% {
386
+ opacity: 0;
387
+ transform: scale(0.95);
388
+ }
389
+ 100% {
390
+ opacity: 1;
391
+ transform: scale(1);
392
+ }
393
+ }
394
+
395
+ @keyframes buttonsFadeIn {
396
+ 0% {
397
+ opacity: 0;
398
+ transform: translateX(-15px);
399
+ }
400
+ 100% {
401
+ opacity: 1;
402
+ transform: translateX(0);
403
+ }
404
+ }
405
+
406
+ /* Adding transition effects for interfaces */
407
+ @keyframes fadeInUp {
408
+ from {
409
+ opacity: 0;
410
+ transform: translateY(20px);
411
+ }
412
+ to {
413
+ opacity: 1;
414
+ transform: translateY(0);
415
+ }
416
+ }
417
+
418
+ @keyframes fadeInRight {
419
+ from {
420
+ opacity: 0;
421
+ transform: translateX(20px);
422
+ }
423
+ to {
424
+ opacity: 1;
425
+ transform: translateX(0);
426
+ }
427
+ }
428
+
429
+ @keyframes fadeInLeft {
430
+ from {
431
+ opacity: 0;
432
+ transform: translateX(-20px);
433
+ }
434
+ to {
435
+ opacity: 1;
436
+ transform: translateX(0);
437
+ }
438
+ }
439
+
440
+ /* Container animation classes that can be dynamically applied */
441
+ .animate-fadeInUp {
442
+ animation: fadeInUp 0.7s forwards;
443
+ }
444
+
445
+ .animate-fadeInRight {
446
+ animation: fadeInRight 0.7s forwards;
447
+ }
448
+
449
+ .animate-fadeInLeft {
450
+ animation: fadeInLeft 0.7s forwards;
451
+ }
452
+
453
+ /* Initial state for containers to be animated */
454
+ .pre-animation {
455
+ opacity: 0;
456
+ }
457
+
458
+ /* Specifically target Gradio elements to make them more mobile-friendly */
459
+ .gradio-container {
460
+ margin-left: 0 !important;
461
+ margin-right: 0 !important;
462
+ }
463
+
464
+ /* Adjust textbox and button widths for mobile */
465
+ .gr-textbox {
466
+ width: 100% !important;
467
+ }
468
+
469
+ .gr-button-primary {
470
+ width: 100% !important;
471
+ margin: 5px 0 !important;
472
+ }
473
+
474
+ /* Make all interface elements take more width on mobile */
475
+ .interface-elements {
476
+ width: 98% !important;
477
+ max-width: 100% !important;
478
+ margin: 0 auto !important;
479
+ }
480
+
481
+ /* Fix padding for form elements */
482
+ input, select, textarea, button {
483
+ box-sizing: border-box !important;
484
+ }
485
+
486
+ /* Ensure minimum touch target size for all clickable elements
487
+ BUT exclude accordion, file input, and chat action buttons */
488
+ button:not(.accordion-button):not(.file-upload button):not([aria-label="Clear"]):not([aria-label="Retry"]):not([aria-label="Undo"]):not(.chat-buttons button),
489
+ .gr-button:not(.file-upload .gr-button):not(.chat-buttons .gr-button),
490
+ a,
491
+ input[type="submit"],
492
+ input[type="button"] {
493
+ min-height: 44px !important; /* Apple's recommended minimum touch target size */
494
+ min-width: 44px !important;
495
+ }
496
+
497
+ /* Remove any unnecessary margins from Gradio components */
498
+ .gradio-container .gr-form > *, .gradio-container .gr-group > * {
499
+ margin-bottom: 8px !important;
500
+ }
501
+
502
+ /* Ensure Gradio container does not exceed viewport width */
503
+ #component-0, #component-1, #component-2, #component-3, #component-4, #component-5,
504
+ #component-6, #component-7, #component-8, #component-9, #component-10 {
505
+ max-width: 100vw !important;
506
+ overflow-x: hidden !important;
507
+ }
508
+
509
+ /* Media Queries for better responsiveness */
510
+ @media screen and (max-width: 768px) {
511
+ body {
512
+ padding: 0 !important; /* Remove body padding on mobile */
513
+ }
514
+
515
+ .dashboard-card {
516
+ width: 100% !important; /* Full width */
517
+ padding: 15px 8px !important; /* Reduced padding */
518
+ margin: 10px auto !important; /* Reduced margin */
519
+ border-radius: 0 !important; /* Optional: Remove border radius for full-width look */
520
+ }
521
+
522
+ .login-form-container {
523
+ width: 100% !important; /* Full width */
524
+ padding: 15px 10px !important; /* Reduced padding */
525
+ margin: 10px auto !important; /* Reduced margin */
526
+ border-radius: 0 !important; /* Optional: Remove border radius for full-width look */
527
+ }
528
+
529
+ .menu-button {
530
+ width: 98% !important; /* Almost full width */
531
+ font-size: 14px !important;
532
+ min-height: 50px !important;
533
+ }
534
+
535
+ .logo-dashboard {
536
+ max-height: 40px;
537
+ top: 10px;
538
+ left: 10px;
539
+ }
540
+
541
+ h1 {
542
+ font-size: 1.5rem !important;
543
+ margin-top: 10px !important;
544
+ margin-bottom: 15px !important;
545
+ }
546
+
547
+ h2 {
548
+ font-size: 1.3rem !important;
549
+ margin-top: 8px !important;
550
+ margin-bottom: 12px !important;
551
+ }
552
+
553
+ h3 {
554
+ font-size: 1.1rem !important;
555
+ margin-top: 6px !important;
556
+ margin-bottom: 10px !important;
557
+ }
558
+
559
+ /* Make chat interface take full width */
560
+ .chat-container, .chatbot-container {
561
+ width: 100% !important;
562
+ margin: 0 !important;
563
+ padding: 0 5px !important;
564
+ }
565
+
566
+ /* Adjust Gradio containers */
567
+ .gradio-container {
568
+ padding: 0 !important;
569
+ }
570
+
571
+ /* Adjust logout button for mobile */
572
+ .logout-btn {
573
+ top: 5px;
574
+ right: 5px;
575
+ padding: 6px 12px !important;
576
+ font-size: 12px !important;
577
+ }
578
+
579
+ .session-info {
580
+ top: 40px;
581
+ right: 5px;
582
+ font-size: 10px;
583
+ }
584
+ }
585
+
586
+ @media screen and (max-width: 480px) {
587
+ .dashboard-card {
588
+ padding: 10px 5px !important; /* Further reduced padding */
589
+ margin: 0 auto !important; /* Remove margin completely */
590
+ border-radius: 0 !important; /* Remove border radius */
591
+ width: 100% !important; /* Full width */
592
+ }
593
+
594
+ .login-form-container {
595
+ padding: 10px 5px !important; /* Further reduced padding */
596
+ margin: 0 auto !important; /* Remove margin completely */
597
+ border-radius: 0 !important; /* Remove border radius */
598
+ width: 100% !important; /* Full width */
599
+ }
600
+
601
+ .menu-button {
602
+ font-size: 13px !important;
603
+ min-height: 45px !important;
604
+ margin: 5px auto !important;
605
+ width: 100% !important; /* Full width */
606
+ border-radius: 6px !important; /* Slightly reduced border radius */
607
+ }
608
+
609
+ .logo-dashboard {
610
+ max-height: 35px;
611
+ top: 5px;
612
+ left: 5px;
613
+ }
614
+
615
+ /* Adjust spacing in forms for mobile */
616
+ input, select, textarea {
617
+ margin-bottom: 8px !important;
618
+ padding: 8px !important;
619
+ width: 100% !important;
620
+ }
621
+
622
+ /* Reduce padding and margins for all content */
623
+ .floating-card, .gr-box, .gr-panel {
624
+ padding: 10px 5px !important;
625
+ margin: 5px 0 !important;
626
+ width: 100% !important;
627
+ }
628
+
629
+ /* Full width for all UI components */
630
+ .gr-form, .gr-input, .gr-button, .gr-box {
631
+ width: 100% !important;
632
+ margin-left: 0 !important;
633
+ margin-right: 0 !important;
634
+ }
635
+ }
636
+
637
+ /* Additional responsive handling for extreme screen sizes */
638
+ @media screen and (max-width: 320px) {
639
+ /* For very small screens like older iPhones */
640
+ .menu-button {
641
+ font-size: 11px !important;
642
+ min-height: 40px !important;
643
+ padding: 8px 5px !important;
644
+ }
645
+
646
+ .dashboard-card {
647
+ padding: 5px 3px !important;
648
+ }
649
+
650
+ h1 {
651
+ font-size: 1.3rem !important;
652
+ }
653
+
654
+ h2 {
655
+ font-size: 1.1rem !important;
656
+ }
657
+
658
+ h3 {
659
+ font-size: 1rem !important;
660
+ }
661
+ }
662
+
663
+ @media screen and (min-width: 1600px) {
664
+ /* For very large screens */
665
+ .dashboard-card {
666
+ max-width: 1000px !important;
667
+ }
668
+
669
+ .menu-button {
670
+ max-width: 400px !important;
671
+ }
672
+ }
673
+
674
+ /* For landscape orientation on mobile */
675
+ @media screen and (max-height: 500px) and (orientation: landscape) {
676
+ .dashboard-menu {
677
+ min-height: auto;
678
+ padding: 5px 0;
679
+ }
680
+
681
+ .dashboard-card {
682
+ min-height: 300px !important;
683
+ }
684
+
685
+ .menu-button {
686
+ min-height: 40px !important;
687
+ margin: 4px auto !important;
688
+ }
689
+ }
690
+
691
+ /* Fixed positions for mobile views */
692
+ .fixed-top {
693
+ position: fixed;
694
+ top: 0;
695
+ left: 0;
696
+ right: 0;
697
+ z-index: 1000;
698
+ }
699
+
700
+ .fixed-bottom {
701
+ position: fixed;
702
+ bottom: 0;
703
+ left: 0;
704
+ right: 0;
705
+ z-index: 1000;
706
+ }
707
+ """
708
+ return css
709
+
710
+ # ----------------- Notification Helpers -----------------
711
+
712
+ def show_success(message: str, duration: float = 10, visible: bool = True, title: str = "Success") -> None:
713
+ """Display a success notification"""
714
+ styled_message = (
715
+ f'<span style="color: #2e7d32; background-color: #c8e6c9; padding: 10px; '
716
+ f'border-radius: 4px; display: block;">{message}</span>'
717
+ )
718
+ gr.Success(styled_message, duration=duration, visible=visible, title=title)
719
+ # Add a small delay to help the modal appear
720
+ time.sleep(0.1)
721
+
722
+ def show_error(message: str, duration: float = 13, visible: bool = True, title: str = "Error") -> None:
723
+ """Display an error notification"""
724
+ styled_message = (
725
+ f'<span style="color: #c62828; background-color: #ffcdd2; padding: 10px; '
726
+ f'border-radius: 4px; display: block;">{message}</span>'
727
+ )
728
+ gr.Error(styled_message, duration=duration, visible=visible, title=title)
729
+ # Add a small delay to help the modal appear
730
+ time.sleep(0.1)
731
+
732
+ # ----------------- Login Form UI -----------------
733
+
734
+ def login_form_ui():
735
+ """Create and return the login form interface"""
736
+ with gr.Blocks() as login_form:
737
+ # USIU logo
738
+ gr.HTML('''
739
+ <div class="logo-container">
740
+ <img src="week5/chatbot_prototype/images/usiu-logo.png" alt="USIU Logo" class="logo" />
741
+ </div>
742
+ ''')
743
+
744
+ gr.Markdown("## Please Login")
745
+ user_id_input = gr.Textbox(
746
+ placeholder="Enter your User ID",
747
+ label="User ID",
748
+ type="email",
749
+ autofocus=True
750
+ )
751
+ # Error message for invalid user ID
752
+ user_id_error = gr.Markdown("", visible=True)
753
+
754
+ password_input = gr.Textbox(
755
+ placeholder="Enter your Password",
756
+ label="Password",
757
+ type="password"
758
+ )
759
+ # Error message for invalid password
760
+ password_error = gr.Markdown("", visible=True)
761
+
762
+ login_btn = gr.Button("Login")
763
+ # Error message for invalid credentials
764
+ login_msg = gr.Markdown("")
765
+ proceed_btn = gr.Button("Proceed", visible=False)
766
+ back_btn = gr.Button("Back to Dashboard")
767
+
768
+ # Hidden field to store session ID
769
+ session_id = gr.Textbox(visible=False)
770
+ # Return the login form along with the necessary components in order.
771
+ return login_form, user_id_input, user_id_error, password_input, password_error, login_btn, login_msg, proceed_btn, back_btn, session_id
772
+
773
+ # ----------------- Authentication Logic -----------------
774
+
775
+ def handle_login(user_id, password):
776
+ """
777
+ Validates login fields and the User ID (which must end with '@usiu.ac.ke').
778
+ Returns appropriate error messages and updates UI components.
779
+ """
780
+ try:
781
+ user_id = user_id.strip() if user_id else ""
782
+ password = password.strip() if password else ""
783
+
784
+ # Both fields empty:
785
+ if user_id == "" and password == "":
786
+ show_error("Please enter your User ID and Password.")
787
+ return (
788
+ "<span style='color:#c62828;'>Please enter your User ID and Password.</span>",
789
+ "", # No field-specific error for User ID.
790
+ "", # No field-specific error for Password.
791
+ gr.update(visible=False),
792
+ gr.update(visible=True),
793
+ "" # No session ID since login failed
794
+ )
795
+ # User ID is empty:
796
+ elif user_id == "":
797
+ show_error("User ID is required.")
798
+ return (
799
+ "",
800
+ "<span style='color:#c62828;'>User ID is required.</span>",
801
+ "",
802
+ gr.update(visible=False),
803
+ gr.update(visible=True),
804
+ "" # No session ID since login failed
805
+ )
806
+ # Password is empty:
807
+ elif password == "":
808
+ show_error("Password is required.")
809
+ return (
810
+ "",
811
+ "",
812
+ "<span style='color:#c62828;'>Password is required.</span>",
813
+ gr.update(visible=False),
814
+ gr.update(visible=True),
815
+ "" # No session ID since login failed
816
+ )
817
+ else:
818
+ # Validate the User ID: must end with "@usiu.ac.ke"
819
+ email_regex = r'^[\w\.-]+@usiu\.ac\.ke$'
820
+ if not re.match(email_regex, user_id):
821
+ show_error("User ID must be a valid \"@usiu.ac.ke\" email address.")
822
+ return (
823
+ "",
824
+ "<span style='color:#c62828;'>User ID must be a valid @usiu.ac.ke email address.</span>",
825
+ "",
826
+ gr.update(visible=False),
827
+ gr.update(visible=True),
828
+ "" # No session ID since login failed
829
+ )
830
+ # Attempt authentication.
831
+ if authenticate(user_id, password):
832
+ # Create a session for the authenticated user
833
+ session_id = create_session(user_id)
834
+
835
+ show_success("Login successful! Click 'Proceed' to continue!", duration=10)
836
+ return (
837
+ "",
838
+ "",
839
+ "",
840
+ gr.update(visible=True), # Show Proceed button.
841
+ gr.update(visible=False), # Hide Login button.
842
+ session_id # Return the session ID for the new session
843
+ )
844
+ else:
845
+ show_error("Invalid credentials. Please try again.")
846
+ return (
847
+ "<span style='color:#c62828;'>Invalid credentials. Please try again.</span>",
848
+ "",
849
+ "",
850
+ gr.update(visible=False),
851
+ gr.update(visible=True),
852
+ "" # No session ID since login failed
853
+ )
854
+ except Exception as e:
855
+ show_error(f"System error: {str(e)}. Please ensure required modules are imported.")
856
+ return (
857
+ "<span style='color:#c62828;'>System error encountered.</span>",
858
+ "",
859
+ "",
860
+ gr.update(visible=False),
861
+ gr.update(visible=True),
862
+ "" # No session ID since login failed
863
+ )
864
+
865
+ def handle_logout(session_id):
866
+ """End the user session and redirect to dashboard"""
867
+ end_session(session_id)
868
+ return [
869
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
870
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
871
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
872
+ gr.update(visible=False, elem_classes="chat-container pre-animation")
873
+ ]
874
+
875
+ # ----------------- Chat Interfaces -----------------
876
+
877
+ def general_chat_ui(system_prompt: str, model: str) -> gr.ChatInterface:
878
+ """Creates a Gradio ChatInterface for general academic chat with RAG."""
879
+
880
+ def general_chat(message: str, chat_history, file) -> str:
881
+ # Ensure chat_history is a list.
882
+ if chat_history is None:
883
+ chat_history = []
884
+
885
+ # If a file is uploaded and the message is empty, replace the empty message with the file name.
886
+ if not message.strip() and file is not None:
887
+ message = f"Uploaded file: {os.path.basename(file.name)}"
888
+
889
+ # If this is a new conversation, insert a conversation header.
890
+ if not chat_history:
891
+ date_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
892
+ conv_header = {
893
+ "role": "header",
894
+ "content": (
895
+ f"<div style='text-align: center; font-weight: bold; font-size: 16px; color: #000;'>"
896
+ f"Conversation started on {date_str}"
897
+ f"</div>"
898
+ ),
899
+ "files": []
900
+ }
901
+ chat_history.insert(0, conv_header) # Insert at the beginning
902
+
903
+ # Start with the initial system prompt.
904
+ messages = [
905
+ {"role": "system", "content": system_prompt, "files": []},
906
+ ]
907
+
908
+ # Add chat history to messages (all messages, including header).
909
+ if chat_history:
910
+ # If the first element is a tuple/list with two elements, assume pair format.
911
+ if isinstance(chat_history[0], (tuple, list)) and len(chat_history[0]) == 2:
912
+ for human, assistant in chat_history:
913
+ messages.append({"role": "user", "content": human, "files": []})
914
+ messages.append({"role": "assistant", "content": assistant, "files": []})
915
+ # Otherwise, if they are dicts, include all messages.
916
+ elif isinstance(chat_history[0], dict):
917
+ for msg in chat_history:
918
+ if msg is None:
919
+ continue
920
+ # Skip header messages when sending to API.
921
+ if msg.get("role") == "header":
922
+ continue
923
+ # Ensure each message has a files field.
924
+ msg_to_send = dict(msg)
925
+ if "files" not in msg_to_send or msg_to_send["files"] is None:
926
+ msg_to_send["files"] = []
927
+ messages.append(msg_to_send)
928
+
929
+ # If a file is uploaded, add its content to the messages.
930
+ if file is not None:
931
+ try:
932
+ file_content = FileProcessor.read_file(file)
933
+ messages.append({
934
+ "role": "user",
935
+ "content": f"Here's the content of the uploaded file:\n\n{file_content}"
936
+ })
937
+ except Exception as e:
938
+ messages.append({
939
+ "role": "user",
940
+ "content": f"Error reading file: {str(e)}",
941
+ "files": []
942
+ })
943
+
944
+ # RAG: Retrieve additional context using GLOBAL_RETRIEVER if available.
945
+ # if GLOBAL_RETRIEVER is not None:
946
+ # try:
947
+ # docs = GLOBAL_RETRIEVER.get_relevant_documents(message)
948
+ # if docs:
949
+ # context_text = "\n\n".join([doc.page_content for doc in docs])
950
+ # # Insert the retrieved context as an additional system message.
951
+ # messages.insert(1, {
952
+ # "role": "system",
953
+ # "content": f"Additional Context:\n{context_text}",
954
+ # "files": []
955
+ # })
956
+ # except Exception as e:
957
+ # print("RAG retrieval failed:", e)
958
+
959
+ # First, ensure no None values are present in the messages list.
960
+ messages = [msg for msg in messages if msg is not None]
961
+
962
+ # RAG Retrieval and Summarization Implementation:
963
+ if GLOBAL_RETRIEVER is not None:
964
+ try:
965
+ # Retrieve relevant documents using the global retriever.
966
+ docs = GLOBAL_RETRIEVER.get_relevant_documents(message)
967
+ if docs:
968
+ summarized_texts = []
969
+ for doc in docs:
970
+ # Summarize each document's page content.
971
+ # Use the summarize_context() function; if it returns None, fallback to an empty string.
972
+ summary = summarize_context(doc.page_content, GPT4O_MODEL) or ""
973
+ # Only add non-empty summaries.
974
+ if summary.strip():
975
+ summarized_texts.append(summary)
976
+ # If we have one or more summaries, combine them and insert as an additional context.
977
+ if summarized_texts:
978
+ context_text = "\n\n".join(summarized_texts)
979
+ messages.insert(1, {
980
+ "role": "system",
981
+ "content": f"Additional Context Summaries:\n{context_text}",
982
+ "files": [] # Always provide an empty list for the "files" key.
983
+ })
984
+ except Exception as e:
985
+ print("RAG retrieval failed:", e)
986
+
987
+
988
+ chat_history.append({"role": "user", "content": message, "files": []})
989
+ messages.append({"role": "user", "content": message, "files": []})
990
+
991
+ # Call OpenAI's ChatCompletion with streaming enabled.
992
+ completion = openai.ChatCompletion.create(
993
+ model=model,
994
+ messages=messages,
995
+ max_tokens=1_000,
996
+ temperature=0.7,
997
+ stream=True,
998
+ )
999
+
1000
+ response = ""
1001
+ for chunk in completion:
1002
+ token = chunk.choices[0].delta.get("content", "")
1003
+ response += token
1004
+ yield response
1005
+
1006
+ iface = gr.ChatInterface(
1007
+ fn=general_chat,
1008
+ additional_inputs=[gr.File(label="Upload a file (optional)", elem_classes="file-upload")],
1009
+ additional_inputs_accordion=gr.Accordion("File Upload", open=False, elem_classes="additional-inputs-accordion"),
1010
+ editable=True,
1011
+ save_history=True,
1012
+ title="General Enquiries",
1013
+ description="<div style='display:flex; justify-content:center;'>Hi there! I'm your AI assistant, here to help you with queries related to USIU. Feel free to ask any questions or seek assistance.</div>",
1014
+ type="messages",
1015
+ cache_mode="eager"
1016
+ )
1017
+ return iface.queue()
1018
+
1019
+ def study_support_ui(system_prompt: str, model: str) -> gr.ChatInterface:
1020
+ """Creates a Gradio ChatInterface for study support with Claude."""
1021
+
1022
+ def study_support_chat(message: str, chat_history, file, session_id) -> str:
1023
+ # Update session activity timestamp
1024
+ if session_id:
1025
+ update_session_activity(session_id)
1026
+
1027
+ # Check if session is still valid
1028
+ session = get_session(session_id)
1029
+ if not session or not session.get("logged_in", False):
1030
+ return "Your session has expired. Please log in again."
1031
+
1032
+ # Ensure chat_history is a list.
1033
+ if chat_history is None:
1034
+ chat_history = []
1035
+
1036
+ # If a file is uploaded and the message is empty, replace the empty message with the file name.
1037
+ if not message.strip() and file is not None:
1038
+ message = f"Uploaded file: {os.path.basename(file.name)}"
1039
+
1040
+ # If this is a new conversation, insert a conversation header.
1041
+ if not chat_history:
1042
+ date_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
1043
+ conv_header = {
1044
+ "role": "header",
1045
+ "content": (
1046
+ f"<div style='text-align: center; font-weight: bold; font-size: 16px; color: #000;'>"
1047
+ f"Conversation started on {date_str}"
1048
+ f"</div>"
1049
+ )
1050
+ }
1051
+ chat_history.insert(0, conv_header)
1052
+
1053
+ # Build the messages list that will be sent to the API.
1054
+ messages = []
1055
+ if chat_history:
1056
+ # If chat_history items are tuples/lists with two elements, unpack them.
1057
+ if isinstance(chat_history[0], (tuple, list)) and len(chat_history[0]) == 2:
1058
+ for human, assistant in chat_history:
1059
+ messages.append({"role": "user", "content": human})
1060
+ messages.append({"role": "assistant", "content": assistant})
1061
+ # Otherwise, if they are dicts, include only "role" and "content" for non-system messages.
1062
+ elif isinstance(chat_history[0], dict):
1063
+ for msg in chat_history:
1064
+ if msg is None:
1065
+ continue
1066
+ if msg.get("role") == "header":
1067
+ continue # Skip header messages.
1068
+ msg_to_send = {"role": msg.get("role"), "content": msg.get("content")}
1069
+ messages.append(msg_to_send)
1070
+
1071
+ # Add the current message.
1072
+ messages.append({"role": "user", "content": message})
1073
+
1074
+ # If a file is uploaded, add its content.
1075
+ if file is not None:
1076
+ try:
1077
+ file_content = FileProcessor.read_file(file)
1078
+ messages.append({
1079
+ "role": "user",
1080
+ "content": f"Here's the content of the uploaded file:\n\n{file_content}"
1081
+ })
1082
+ except Exception as e:
1083
+ messages.append({
1084
+ "role": "user",
1085
+ "content": f"Error reading file: {str(e)}"
1086
+ })
1087
+
1088
+ # Implement a retry mechanism to handle overloaded errors.
1089
+ max_retries = 7
1090
+ delay = 3 # seconds
1091
+ attempt = 0
1092
+ while attempt < max_retries:
1093
+ try:
1094
+ # Use Claude's API for the study support interface
1095
+ result = claude.messages.stream(
1096
+ model=model,
1097
+ max_tokens=63_500,
1098
+ system=system_prompt,
1099
+ messages=messages,
1100
+ extra_query={"extended_thinking": True}
1101
+ )
1102
+ response = ""
1103
+ with result as stream:
1104
+ for text in stream.text_stream:
1105
+ response += text
1106
+ yield response # Yield incremental responses.
1107
+
1108
+ break # Exit the retry loop if successful.
1109
+ except Exception as e:
1110
+ # Check if error indicates the API is overloaded.
1111
+ if "overloaded" in str(e).lower():
1112
+ attempt += 1
1113
+ yield f"Service unavailable. Retrying in {delay} seconds... (attempt {attempt}/{max_retries})\n"
1114
+ time.sleep(delay)
1115
+ else:
1116
+ print(f"An error occurred: {str(e)}")
1117
+ break
1118
+ else:
1119
+ yield "The service is currently unavailable. Please try again later."
1120
+
1121
+ iface = gr.ChatInterface(
1122
+ fn=study_support_chat,
1123
+ additional_inputs=[
1124
+ gr.File(label="Upload a file (optional)", elem_classes="file-upload"),
1125
+ gr.Textbox(visible=False) # Hidden input for session_id
1126
+ ],
1127
+ additional_inputs_accordion=gr.Accordion("File Upload", open=False, elem_classes="additional-inputs-accordion"),
1128
+ editable=True,
1129
+ save_history=True,
1130
+ title="Study Bud",
1131
+ description="<div style='display:flex; justify-content:center;'>Hi there! I'm your AI study bud. Feel free to ask questions, share resources, and get personalized support.</div>",
1132
+ type="messages",
1133
+ cache_mode="eager"
1134
+ )
1135
+ return iface.queue()
1136
+
1137
+ # ---------- Dashboard UI ----------
1138
+
1139
+ def dashboard_ui(general_chat_prompt: str, general_model: str,
1140
+ study_prompt: str, study_model: str, retriever=None):
1141
+ """
1142
+ Creates a dashboard with professional styling, two main menus (General Chat and Study Support),
1143
+ and smooth transitions between views. For Study Support, a login form (with authentication)
1144
+ is shown before revealing the chat interface.
1145
+
1146
+ Args:
1147
+ general_chat_prompt: System prompt for general enquiries
1148
+ general_model: Model ID for general chat (e.g., GPT4O_MODEL)
1149
+ study_prompt: System prompt for study support
1150
+ study_model: Model ID for study support (e.g., CLAUDE_MODEL)
1151
+ retriever: The retriever to use for RAG in general chat
1152
+ """
1153
+ global GLOBAL_RETRIEVER
1154
+ GLOBAL_RETRIEVER = retriever
1155
+
1156
+ # Retrieve the existing chat interfaces.
1157
+ general_chat_component = general_chat_ui(general_chat_prompt, general_model)
1158
+ study_support_component = study_support_ui(study_prompt, study_model)
1159
+ # Retrieve the login form.
1160
+ (login_form_component, user_id_input, user_id_error, password_input, password_error,
1161
+ login_btn, login_msg, proceed_btn, login_back_btn, session_id) = login_form_ui()
1162
+
1163
+ with gr.Blocks(css=custom_css()) as dashboard:
1164
+ # Inject JavaScript for animations, loading effects, and inactivity timer
1165
+ gr.HTML('''
1166
+ <script>
1167
+ document.addEventListener("DOMContentLoaded", function() {
1168
+ // Create and style the spinner element
1169
+ let spinner = document.createElement("div");
1170
+ spinner.className = "loader";
1171
+ spinner.innerHTML = '<div class="cube"></div>';
1172
+ document.body.appendChild(spinner);
1173
+
1174
+ // Create inactivity warning modal
1175
+ let warningModal = document.createElement("div");
1176
+ warningModal.className = "inactive-warning";
1177
+ warningModal.innerHTML = `
1178
+ <h3>Session Timeout Warning</h3>
1179
+ <p>Your session will expire due to inactivity in <span id="countdown">60</span> seconds.</p>
1180
+ <p>Click the button below to continue your session.</p>
1181
+ <button id="stay-active">Stay Active</button>
1182
+ `;
1183
+ document.body.appendChild(warningModal);
1184
+
1185
+ // Create overlay for modal
1186
+ let overlay = document.createElement("div");
1187
+ overlay.className = "overlay";
1188
+ document.body.appendChild(overlay);
1189
+
1190
+ // Variables for inactivity tracking
1191
+ let inactivityTimer;
1192
+ let lastActivity = Date.now();
1193
+ let countdownTimer;
1194
+ let countdownTime = 60; // Countdown from 60 seconds
1195
+
1196
+ // Function to reset inactivity timer
1197
+ function resetInactivityTimer() {
1198
+ lastActivity = Date.now();
1199
+
1200
+ // Clear existing timer
1201
+ if (inactivityTimer) {
1202
+ clearTimeout(inactivityTimer);
1203
+ }
1204
+
1205
+ // Set new timer - show warning after 14 minutes of inactivity
1206
+ inactivityTimer = setTimeout(function() {
1207
+ showInactivityWarning();
1208
+ }, 14 * 60 * 1000); // 14 minutes in milliseconds
1209
+ }
1210
+
1211
+ // Function to show inactivity warning
1212
+ function showInactivityWarning() {
1213
+ overlay.style.display = "block";
1214
+ warningModal.style.display = "block";
1215
+
1216
+ // Start countdown
1217
+ countdownTime = 60;
1218
+ document.getElementById("countdown").textContent = countdownTime;
1219
+
1220
+ countdownTimer = setInterval(function() {
1221
+ countdownTime--;
1222
+ document.getElementById("countdown").textContent = countdownTime;
1223
+
1224
+ if (countdownTime <= 0) {
1225
+ clearInterval(countdownTimer);
1226
+ // Find and click logout button to end session
1227
+ const logoutButton = document.querySelector('.logout-btn');
1228
+ if (logoutButton) {
1229
+ logoutButton.click();
1230
+ }
1231
+ hideInactivityWarning();
1232
+ }
1233
+ }, 1000);
1234
+ }
1235
+
1236
+ // Function to hide inactivity warning
1237
+ function hideInactivityWarning() {
1238
+ overlay.style.display = "none";
1239
+ warningModal.style.display = "none";
1240
+ if (countdownTimer) {
1241
+ clearInterval(countdownTimer);
1242
+ }
1243
+ }
1244
+
1245
+ // Event listener for "Stay Active" button
1246
+ document.getElementById("stay-active").addEventListener("click", function() {
1247
+ hideInactivityWarning();
1248
+ resetInactivityTimer();
1249
+ });
1250
+
1251
+ // Track user activity
1252
+ ["mousemove", "keydown", "click", "scroll", "touchstart"].forEach(function(event) {
1253
+ document.addEventListener(event, function() {
1254
+ // Only reset timer if we're not showing the warning
1255
+ if (warningModal.style.display !== "block") {
1256
+ resetInactivityTimer();
1257
+ }
1258
+ });
1259
+ });
1260
+
1261
+ // Initial setup of inactivity timer
1262
+ resetInactivityTimer();
1263
+
1264
+ // Show the spinner when needed
1265
+ spinner.style.display = "none"; // Initially hidden
1266
+
1267
+ // Function to show spinner
1268
+ function showSpinner() {
1269
+ spinner.style.display = "block";
1270
+ }
1271
+
1272
+ // Function to hide spinner
1273
+ function hideSpinner() {
1274
+ spinner.style.display = "none";
1275
+ }
1276
+
1277
+ // Function to handle animation transitions
1278
+ function setupAnimations() {
1279
+ // Get all containers that might be animated
1280
+ const containers = document.querySelectorAll('.container, .chat-container');
1281
+
1282
+ // Add transition end listener to each container
1283
+ containers.forEach(container => {
1284
+ container.addEventListener('animationend', function(e) {
1285
+ // When animation completes, remove the animation class
1286
+ if (e.animationName.includes('fadeIn')) {
1287
+ this.classList.remove('animate-fadeInUp', 'animate-fadeInRight', 'animate-fadeInLeft');
1288
+ }
1289
+ });
1290
+ });
1291
+ }
1292
+
1293
+ // Setup animations after a short delay to ensure DOM is ready
1294
+ setTimeout(setupAnimations, 2000);
1295
+
1296
+ // Add event listeners to all buttons that should trigger the spinner
1297
+ setTimeout(function(){
1298
+ // Login and Proceed buttons with spinner
1299
+ const loginBtn = document.querySelector('button:contains("Login")');
1300
+ const proceedBtn = document.querySelector('button:contains("Proceed")');
1301
+
1302
+ if (loginBtn) {
1303
+ loginBtn.addEventListener("click", function() {
1304
+ showSpinner(); // Show spinner when login button is clicked
1305
+
1306
+ if (!this.classList.contains("loading")) {
1307
+ const originalText = this.textContent;
1308
+ this.setAttribute("data-original-text", originalText);
1309
+ this.textContent = "";
1310
+ this.classList.add("loading");
1311
+
1312
+ // Create spinner inside button
1313
+ const btnSpinner = document.createElement("div");
1314
+ btnSpinner.className = "btn-spinner";
1315
+ btnSpinner.style.width = "20px";
1316
+ btnSpinner.style.height = "20px";
1317
+ btnSpinner.style.border = "3px solid rgba(255,255,255,0.3)";
1318
+ btnSpinner.style.borderRadius = "50%";
1319
+ btnSpinner.style.borderTopColor = "#fff";
1320
+ btnSpinner.style.animation = "spin 1s linear infinite";
1321
+ btnSpinner.style.display = "inline-block";
1322
+ btnSpinner.style.verticalAlign = "middle";
1323
+ this.appendChild(btnSpinner);
1324
+
1325
+ // Reset button after 3 seconds if no response
1326
+ setTimeout(() => {
1327
+ if (this.classList.contains("loading")) {
1328
+ this.classList.remove("loading");
1329
+ this.textContent = this.getAttribute("data-original-text");
1330
+ }
1331
+ hideSpinner(); // Hide spinner after timeout
1332
+ }, 3000);
1333
+ }
1334
+ });
1335
+ }
1336
+
1337
+ if (proceedBtn) {
1338
+ proceedBtn.addEventListener("click", function() {
1339
+ showSpinner(); // Show spinner when proceed button is clicked
1340
+
1341
+ if (!this.classList.contains("loading")) {
1342
+ const originalText = this.textContent;
1343
+ this.setAttribute("data-original-text", originalText);
1344
+ this.textContent = "";
1345
+ this.classList.add("loading");
1346
+
1347
+ // Create spinner inside button
1348
+ const btnSpinner = document.createElement("div");
1349
+ btnSpinner.className = "btn-spinner";
1350
+ btnSpinner.style.width = "20px";
1351
+ btnSpinner.style.height = "20px";
1352
+ btnSpinner.style.border = "3px solid rgba(255,255,255,0.3)";
1353
+ btnSpinner.style.borderRadius = "50%";
1354
+ btnSpinner.style.borderTopColor = "#fff";
1355
+ btnSpinner.style.animation = "spin 1s linear infinite";
1356
+ btnSpinner.style.display = "inline-block";
1357
+ btnSpinner.style.verticalAlign = "middle";
1358
+ this.appendChild(btnSpinner);
1359
+
1360
+ // Reset button after 3 seconds if no response
1361
+ setTimeout(() => {
1362
+ if (this.classList.contains("loading")) {
1363
+ this.classList.remove("loading");
1364
+ this.textContent = this.getAttribute("data-original-text");
1365
+ }
1366
+ hideSpinner(); // Hide spinner after timeout
1367
+ }, 3000);
1368
+ }
1369
+ });
1370
+ }
1371
+
1372
+ // Scale effect for dashboard buttons
1373
+ const dashboardButtons = document.querySelectorAll('.menu-button, button:contains("Back to Dashboard")');
1374
+ dashboardButtons.forEach(function(btn) {
1375
+ btn.addEventListener("click", function() {
1376
+ this.style.transform = "scale(1.1)";
1377
+ setTimeout(() => {
1378
+ this.style.transform = "";
1379
+ }, 200);
1380
+ });
1381
+ });
1382
+ }, 2000);
1383
+
1384
+ // Add keyframes for spinner animation
1385
+ const style = document.createElement('style');
1386
+ style.textContent = `
1387
+ @keyframes spin {
1388
+ 0% { transform: rotate(0deg); }
1389
+ 100% { transform: rotate(360deg); }
1390
+ }
1391
+ `;
1392
+ document.head.appendChild(style);
1393
+ });
1394
+ </script>
1395
+ ''')
1396
+
1397
+ # Logo for all pages - positioned in top-left corner
1398
+ gr.HTML('''
1399
+ <div class="logo-dashboard">
1400
+ <!-- USIU Logo Placeholder - Replace with actual logo path -->
1401
+ <img src="week5/chatbot_prototype/images/usiu-logo.png" alt="USIU Logo" class="logo" />
1402
+ </div>
1403
+ ''')
1404
+
1405
+ # Define containers for different views with initial animation state
1406
+ dashboard_container = gr.Column(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInUp")
1407
+ general_container = gr.Column(visible=False, elem_classes="chat-container pre-animation")
1408
+ login_container = gr.Column(visible=False, elem_classes="container floating-card login-form-container pre-animation")
1409
+ study_container = gr.Column(visible=False, elem_classes="chat-container pre-animation")
1410
+
1411
+ # Logout Button Component for Study Support (initially hidden)
1412
+ with gr.Column(visible=False) as logout_component:
1413
+ # Session information display (hidden textbox for now)
1414
+ current_user = gr.Textbox(visible=False)
1415
+ # Create a dedicated logout button with custom styling
1416
+ logout_btn = gr.Button("Logout", elem_classes="logout-btn")
1417
+ # An additional HTML component for JS updates (used in the Proceed callback)
1418
+ js_update = gr.HTML("", visible=False)
1419
+
1420
+ # ------------- Dashboard View -------------
1421
+ with dashboard_container:
1422
+ gr.Markdown("<h1 class='with-logo'>AI Assistance Dashboard</h1>")
1423
+ with gr.Column(elem_classes="dashboard-menu"):
1424
+ gen_button = gr.Button("General Enquiries", elem_classes="menu-button")
1425
+ study_button = gr.Button("Study with AI", elem_classes="menu-button")
1426
+
1427
+ # ------------- General Academic Chat View -------------
1428
+ with general_container:
1429
+ back_gen = gr.Button("Back to Dashboard")
1430
+ general_chat_component.render()
1431
+
1432
+ # ------------- Login Form (for Study Support) View -------------
1433
+ with login_container:
1434
+ login_form_component.render()
1435
+
1436
+ # ------------- Study Support Chat View -------------
1437
+ with study_container:
1438
+ back_study = gr.Button("Back to Dashboard")
1439
+ study_support_component.render()
1440
+
1441
+ # Set session ID for the study chat interface
1442
+ # This ensures the session is refreshed on user interaction
1443
+ session_id_for_study = gr.Textbox(visible=False)
1444
+
1445
+ # ------------- Navigation Callbacks -------------
1446
+ # When "General Academic Chat" is selected from the dashboard.
1447
+ gen_button.click(
1448
+ lambda: [
1449
+ gr.update(visible=False),
1450
+ gr.update(visible=True, elem_classes="chat-container animate-fadeInRight"),
1451
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1452
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1453
+ gr.update(visible=False) # Hide logout component
1454
+ ],
1455
+ outputs=[dashboard_container, general_container, login_container, study_container, logout_component]
1456
+ )
1457
+
1458
+ # When "Study Support Chat" is selected from the dashboard, show the login view with animation.
1459
+ study_button.click(
1460
+ lambda: [
1461
+ gr.update(visible=False),
1462
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1463
+ gr.update(visible=True, elem_classes="container floating-card login-form-container animate-fadeInRight"),
1464
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1465
+ gr.update(visible=False) # Hide logout component
1466
+ ],
1467
+ outputs=[dashboard_container, general_container, login_container, study_container, logout_component]
1468
+ )
1469
+
1470
+ # "Back to Dashboard" buttons with animation.
1471
+ back_gen.click(
1472
+ lambda: [
1473
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
1474
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1475
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1476
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1477
+ gr.update(visible=False) # Hide logout component
1478
+ ],
1479
+ outputs=[dashboard_container, general_container, login_container, study_container, logout_component]
1480
+ )
1481
+
1482
+ back_study.click(
1483
+ lambda: [
1484
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
1485
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1486
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1487
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1488
+ gr.update(visible=False) # Hide logout component
1489
+ ],
1490
+ outputs=[dashboard_container, general_container, login_container, study_container, logout_component]
1491
+ )
1492
+
1493
+ login_back_btn.click(
1494
+ lambda: [
1495
+ gr.update(visible=True, elem_classes="container floating-card dashboard-card animate-fadeInLeft"),
1496
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1497
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1498
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1499
+ gr.update(visible=False) # Hide logout component
1500
+ ],
1501
+ outputs=[dashboard_container, general_container, login_container, study_container, logout_component]
1502
+ )
1503
+
1504
+ # ------------- Authentication Logic -------------
1505
+ login_btn.click(
1506
+ handle_login,
1507
+ inputs=[user_id_input, password_input],
1508
+ outputs=[login_msg, user_id_error, password_error, proceed_btn, login_btn, session_id]
1509
+ )
1510
+
1511
+ # When "Proceed" is clicked (after successful authentication), show the study support chat with animation.
1512
+ def proceed_to_study(session_id_value):
1513
+ # Get user info from the session
1514
+ session = get_session(session_id_value)
1515
+ user_email = session["user_id"] if session else "Guest"
1516
+
1517
+ # Updated JS code wrapped in <script> tags to update the user email display.
1518
+ js_code = f"<script>document.getElementById('user-email').textContent = '{user_email}';</script>"
1519
+
1520
+ # Start tracking activity for this session
1521
+ update_session_activity(session_id_value)
1522
+
1523
+ return [
1524
+ gr.update(visible=False),
1525
+ gr.update(visible=False, elem_classes="chat-container pre-animation"),
1526
+ gr.update(visible=False, elem_classes="container floating-card login-form-container pre-animation"),
1527
+ gr.update(visible=True, elem_classes="chat-container animate-fadeInRight"),
1528
+ gr.update(visible=True), # Show logout component
1529
+ user_email, # Update current user display
1530
+ session_id_value, # Pass session ID to the study chat interface
1531
+ js_code # Update the new HTML component (js_update)
1532
+ ]
1533
+
1534
+ proceed_btn.click(
1535
+ proceed_to_study,
1536
+ inputs=[session_id],
1537
+ outputs=[
1538
+ dashboard_container,
1539
+ general_container,
1540
+ login_container,
1541
+ study_container,
1542
+ logout_component,
1543
+ current_user,
1544
+ session_id_for_study,
1545
+ js_update # Replaces gr.JavaScript.update()
1546
+ ]
1547
+ )
1548
+
1549
+ # Logout button functionality – using the dedicated logout button (logout_btn)
1550
+ logout_btn.click(
1551
+ handle_logout,
1552
+ inputs=[session_id],
1553
+ outputs=[dashboard_container, general_container, login_container, study_container]
1554
+ )
1555
+
1556
+ # NOTE: Removed the .change() callback on the ChatInterface (study_support_component)
1557
+ # because ChatInterface objects do not support the .change() method.
1558
+ # Session activity is now updated within the chat callback of study_support_chat.
1559
+
1560
+ return dashboard.queue()
speech.wav ADDED
File without changes
system_prompts/__init__.py ADDED
File without changes
system_prompts/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (266 Bytes). View file
 
system_prompts/__pycache__/prompt_for_general_chat.cpython-311.pyc ADDED
Binary file (7.22 kB). View file
 
system_prompts/__pycache__/prompt_for_study_support.cpython-311.pyc ADDED
Binary file (7.55 kB). View file
 
system_prompts/__pycache__/prompts_manager.cpython-311.pyc ADDED
Binary file (27.2 kB). View file
 
system_prompts/__pycache__/system_prompts.cpython-311.pyc ADDED
Binary file (14.9 kB). View file