Koalar commited on
Commit
a0eebcb
Β·
verified Β·
1 Parent(s): 168561c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +672 -0
app.py ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chatbot_demo.py
2
+ import gradio as gr
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import List, Tuple, Optional
6
+ import os
7
+ import socket
8
+
9
+ from kallam.app.chatbot_manager import ChatbotManager
10
+
11
+ mgr = ChatbotManager(log_level="INFO")
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # INLINE SVG for icons
16
+ CABBAGE_SVG = """
17
+ <svg width="128" height="128" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"
18
+ role="img" aria-label="cabbage">
19
+ <defs>
20
+ <linearGradient id="leaf" x1="0" x2="0" y1="0" y2="1">
21
+ <stop offset="0%" stop-color="#9be58b"/>
22
+ <stop offset="100%" stop-color="#5cc46a"/>
23
+ </linearGradient>
24
+ </defs>
25
+ <g fill="none">
26
+ <circle cx="32" cy="32" r="26" fill="url(#leaf)"/>
27
+ <path d="M12,34 C18,28 22,26 28,27 C34,28 38,32 44,30 C48,29 52,26 56,22"
28
+ stroke="#2e7d32" stroke-width="3" stroke-linecap="round"/>
29
+ <path d="M10,40 C18,36 22,42 28,42 C34,42 38,38 44,40 C50,42 54,40 58,36"
30
+ stroke="#2e7d32" stroke-width="3" stroke-linecap="round"/>
31
+ <path d="M24 24 C28 20 36 20 40 24 C44 28 44 36 32 38 C20 36 20 28 24 24 Z"
32
+ fill="#bff5b6"/>
33
+ </g>
34
+ </svg>
35
+ """
36
+
37
+ # -----------------------
38
+ # Core handlers
39
+ # -----------------------
40
+ def _session_status(session_id: str) -> str:
41
+ """Get current session status using mgr.get_session()"""
42
+ if not session_id:
43
+ return "πŸ”΄ **No Active Session** - Click **New Session** to start"
44
+
45
+ try:
46
+ # Use same method as simple app
47
+ now = datetime.now()
48
+ s = mgr.get_session(session_id) or {}
49
+ ts = s.get("timestamp", now.strftime("%d %b %Y | %I:%M %p"))
50
+ model = s.get("model_used", "Orchestrated SEA-Lion")
51
+ total = s.get("total_messages", 0)
52
+ saved_memories = s.get("saved_memories") or "General consultation"
53
+
54
+ return f"""
55
+ 🟒 **Session:** `{session_id[:8]}...`
56
+ πŸ₯ **Profile:** {saved_memories[:50]}{"..." if len(saved_memories) > 50 else ""}
57
+ πŸ“… **Created:** {ts}
58
+ πŸ’¬ **Messages:** {total}
59
+ πŸ€– **Model:** {model}
60
+ """.strip()
61
+ except Exception as e:
62
+ logger.error(f"Error getting session status: {e}")
63
+ return f"❌ **Error loading session:** {session_id[:8]}..."
64
+
65
+ def start_new_session(health_profile: str = ""):
66
+ """Create new session using mgr - same as simple app"""
67
+ try:
68
+ sid = mgr.start_session(saved_memories=health_profile.strip() or None)
69
+ status = _session_status(sid)
70
+
71
+ # Initial welcome message
72
+ welcome_msg = {
73
+ "role": "assistant",
74
+ "content": """Hello! I'm KaLLaM 🌿, your caring AI health advisor πŸ’–
75
+
76
+ I can communicate in both **Thai** and **English**. I'm here to support your health and well-being with personalized advice. How are you feeling today? 😊
77
+
78
+ ΰΈͺΰΈ§ΰΈ±ΰΈͺΰΈ”ΰΈ΅ΰΈ„ΰΉˆΰΈ°! ΰΈ‰ΰΈ±ΰΈ™ΰΈŠΰΈ·ΰΉˆΰΈ­ΰΈΰΈ°ΰΈ«ΰΈ₯่ำ 🌿 ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ΰΈ—ΰΈ΅ΰΉˆΰΈ›ΰΈ£ΰΈΆΰΈΰΈ©ΰΈ²ΰΈ”ΰΉ‰ΰΈ²ΰΈ™ΰΈͺΰΈΈΰΈ‚ΰΈ ΰΈ²ΰΈž AI ΰΈ—ΰΈ΅ΰΉˆΰΈˆΰΈ°ΰΈ„ΰΈ­ΰΈ’ΰΈ”ΰΈΉΰΉΰΈ₯ΰΈ„ΰΈΈΰΈ“ πŸ’– ΰΈ‰ΰΈ±ΰΈ™ΰΈͺΰΈ²ΰΈ‘ΰΈ²ΰΈ£ΰΈ–ΰΈͺื่อΰΈͺารได้ทั้งภาษาไทฒแΰΈ₯ะภาษาอังก฀ษ ΰΈ§ΰΈ±ΰΈ™ΰΈ™ΰΈ΅ΰΉ‰ΰΈ£ΰΈΉΰΉ‰ΰΈͺΰΈΆΰΈΰΈ’ΰΈ±ΰΈ‡ΰΉ„ΰΈ‡ΰΈšΰΉ‰ΰΈ²ΰΈ‡ΰΈ„ΰΈ°? 😊"""
79
+ }
80
+
81
+ history = [welcome_msg]
82
+ result_msg = f"βœ… **New Session Created Successfully!**\n\nπŸ†” Session ID: `{sid}`"
83
+ if health_profile.strip():
84
+ result_msg += f"\nπŸ₯ **Health Profile:** Applied successfully"
85
+
86
+ return sid, history, "", status, result_msg
87
+ except Exception as e:
88
+ logger.error(f"Error creating new session: {e}")
89
+ return "", [], "", "❌ **Failed to create session**", f"❌ **Error:** {e}"
90
+
91
+ def send_message(user_msg: str, history: list, session_id: str):
92
+ """Send message using mgr - same as simple app"""
93
+ # Defensive: auto-create session if missing (same as simple app)
94
+ if not session_id:
95
+ logger.warning("No session found, auto-creating...")
96
+ sid, history, _, status, _ = start_new_session("")
97
+ history.append({"role": "assistant", "content": "πŸ”„ **New session created automatically.** You can now continue chatting!"})
98
+ return history, "", sid, status
99
+
100
+ if not user_msg.strip():
101
+ return history, "", session_id, _session_status(session_id)
102
+
103
+ try:
104
+ # Add user message
105
+ history = history + [{"role": "user", "content": user_msg}]
106
+
107
+ # Get bot response using mgr (same as simple app)
108
+ bot_response = mgr.handle_message(
109
+ session_id=session_id,
110
+ user_message=user_msg
111
+ )
112
+
113
+ # Add bot response
114
+ history = history + [{"role": "assistant", "content": bot_response}]
115
+
116
+ return history, "", session_id, _session_status(session_id)
117
+
118
+ except Exception as e:
119
+ logger.error(f"Error processing message: {e}")
120
+ error_msg = {"role": "assistant", "content": f"❌ **Error:** Unable to process your message. Please try again.\n\nDetails: {e}"}
121
+ history = history + [error_msg]
122
+ return history, "", session_id, _session_status(session_id)
123
+
124
+ def update_health_profile(session_id: str, health_profile: str):
125
+ """Update health profile for current session using mgr's database access"""
126
+ if not session_id:
127
+ return "❌ **No active session**", _session_status(session_id)
128
+
129
+ if not health_profile.strip():
130
+ return "❌ **Please provide health information**", _session_status(session_id)
131
+
132
+ try:
133
+ # Use mgr's database path (same pattern as simple app would use)
134
+ from kallam.infra.db import sqlite_conn
135
+ with sqlite_conn(str(mgr.db_path)) as conn:
136
+ conn.execute(
137
+ "UPDATE sessions SET saved_memories = ?, last_activity = ? WHERE session_id = ?",
138
+ (health_profile.strip(), datetime.now().isoformat(), session_id),
139
+ )
140
+
141
+ result = f"βœ… **Health Profile Updated Successfully!**\n\nπŸ“ **Updated Information:** {health_profile.strip()[:100]}{'...' if len(health_profile.strip()) > 100 else ''}"
142
+ return result, _session_status(session_id)
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error updating health profile: {e}")
146
+ return f"❌ **Error updating profile:** {e}", _session_status(session_id)
147
+
148
+ def clear_session(session_id: str):
149
+ """Clear current session using mgr"""
150
+ if not session_id:
151
+ return "", [], "", "πŸ”΄ **No active session to clear**", "❌ **No active session**"
152
+
153
+ try:
154
+ # Check if mgr has delete_session method, otherwise handle gracefully
155
+ if hasattr(mgr, 'delete_session'):
156
+ mgr.delete_session(session_id)
157
+ else:
158
+ # Fallback: just clear the session data if method doesn't exist
159
+ logger.warning("delete_session method not available, clearing session state only")
160
+
161
+ return "", [], "", "πŸ”΄ **Session cleared - Create new session to continue**", f"βœ… **Session `{session_id[:8]}...` cleared successfully**"
162
+ except Exception as e:
163
+ logger.error(f"Error clearing session: {e}")
164
+ return session_id, [], "", _session_status(session_id), f"❌ **Error clearing session:** {e}"
165
+
166
+ def force_summary(session_id: str):
167
+ """Force summary using mgr (same as simple app)"""
168
+ if not session_id:
169
+ return "❌ No active session."
170
+ try:
171
+ if hasattr(mgr, 'summarize_session'):
172
+ s = mgr.summarize_session(session_id)
173
+ return f"πŸ“‹ Summary updated:\n\n{s}"
174
+ else:
175
+ return "❌ Summarize function not available."
176
+ except Exception as e:
177
+ return f"❌ Failed to summarize: {e}"
178
+
179
+ def lock_inputs():
180
+ """Lock inputs during processing (same as simple app)"""
181
+ return gr.update(interactive=False), gr.update(interactive=False)
182
+
183
+ def unlock_inputs():
184
+ """Unlock inputs after processing (same as simple app)"""
185
+ return gr.update(interactive=True), gr.update(interactive=True)
186
+
187
+ # -----------------------
188
+ # UI with improved architecture and greenish cream styling - LIGHT MODE DEFAULT
189
+ # -----------------------
190
+ def create_app() -> gr.Blocks:
191
+ # Enhanced CSS with greenish cream color scheme, fixed positioning, and light mode defaults
192
+ custom_css = """
193
+ :root {
194
+ --kallam-primary: #659435;
195
+ --kallam-secondary: #5ea0bd;
196
+ --kallam-accent: #b8aa54;
197
+ --kallam-light: #f8fdf5;
198
+ --kallam-dark: #2d3748;
199
+ --kallam-cream: #f5f7f0;
200
+ --kallam-green-cream: #e8f4e0;
201
+ --kallam-border-cream: #d4e8c7;
202
+ --shadow-soft: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
203
+ --shadow-medium: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
204
+ --border-radius: 12px;
205
+ --transition: all 0.3s ease;
206
+ }
207
+
208
+ /* Force light mode styles - Override any dark mode defaults */
209
+ body, .gradio-container, .app {
210
+ background-color: #ffffff !important;
211
+ color: #2d3748 !important;
212
+ }
213
+
214
+ /* Ensure light backgrounds for all major containers */
215
+ .block, .form, .gap {
216
+ background-color: #ffffff !important;
217
+ color: #2d3748 !important;
218
+ }
219
+
220
+ /* Light mode for input elements */
221
+ input, textarea, select {
222
+ background-color: #ffffff !important;
223
+ border: 1px solid #d1d5db !important;
224
+ color: #2d3748 !important;
225
+ }
226
+
227
+ input:focus, textarea:focus, select:focus {
228
+ border-color: var(--kallam-primary) !important;
229
+ box-shadow: 0 0 0 3px rgba(101, 148, 53, 0.1) !important;
230
+ }
231
+
232
+ /* Ensure dark mode styles don't override in light mode */
233
+ html:not(.dark) .dark {
234
+ display: none !important;
235
+ }
236
+
237
+ .gradio-container {
238
+ max-width: 100% !important;
239
+ width: 100% !important;
240
+ margin: 0 auto !important;
241
+ min-height: 100vh;
242
+ background-color: #ffffff !important;
243
+ }
244
+
245
+ .main-layout {
246
+ display: flex !important;
247
+ min-height: calc(100vh - 2rem) !important;
248
+ gap: 1.5rem !important;
249
+ }
250
+
251
+ .fixed-sidebar {
252
+ width: 320px !important;
253
+ min-width: 320px !important;
254
+ max-width: 320px !important;
255
+ background: #ffffff !important;
256
+ backdrop-filter: blur(10px) !important;
257
+ border-radius: var(--border-radius) !important;
258
+ border: 3px solid var(--kallam-primary) !important;
259
+ box-shadow: var(--shadow-soft) !important;
260
+ padding: 1.5rem !important;
261
+ height: fit-content !important;
262
+ position: sticky !important;
263
+ top: 1rem !important;
264
+ overflow: visible !important;
265
+ }
266
+
267
+ .main-content {
268
+ flex: 1 !important;
269
+ min-width: 0 !important;
270
+ }
271
+
272
+ .kallam-header {
273
+ background: linear-gradient(135deg, var(--kallam-secondary) 0%, var(--kallam-primary) 50%, var(--kallam-accent) 100%);
274
+ border-radius: var(--border-radius);
275
+ padding: 2rem;
276
+ margin-bottom: 1.5rem;
277
+ text-align: center;
278
+ box-shadow: var(--shadow-medium);
279
+ position: relative;
280
+ overflow: hidden;
281
+ }
282
+
283
+ .kallam-header h1 {
284
+ color: white !important;
285
+ font-size: 2.5rem !important;
286
+ font-weight: 700 !important;
287
+ margin: 0 !important;
288
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2);
289
+ position: relative;
290
+ z-index: 1;
291
+ }
292
+
293
+ .kallam-subtitle {
294
+ color: rgba(255,255,255,0.9) !important;
295
+ font-size: 1.1rem !important;
296
+ margin-top: 0.5rem !important;
297
+ position: relative;
298
+ z-index: 1;
299
+ }
300
+
301
+ .btn {
302
+ border-radius: 8px !important;
303
+ font-weight: 600 !important;
304
+ padding: 0.75rem 1.5rem !important;
305
+ transition: var(--transition) !important;
306
+ border: none !important;
307
+ box-shadow: var(--shadow-soft) !important;
308
+ cursor: pointer !important;
309
+ }
310
+
311
+ .btn:hover {
312
+ transform: translateY(-2px) !important;
313
+ box-shadow: var(--shadow-medium) !important;
314
+ }
315
+
316
+ .btn.btn-primary {
317
+ background: linear-gradient(135deg, var(--kallam-primary) 0%, var(--kallam-secondary) 100%) !important;
318
+ color: white !important;
319
+ }
320
+
321
+ .btn.btn-secondary {
322
+ background: #f8f9fa !important;
323
+ color: #2d3748 !important;
324
+ border: 1px solid #d1d5db !important;
325
+ }
326
+
327
+ .chat-container {
328
+ background: var(--kallam-green-cream) !important;
329
+ border-radius: var(--border-radius) !important;
330
+ border: 2px solid var(--kallam-border-cream) !important;
331
+ box-shadow: var(--shadow-medium) !important;
332
+ overflow: hidden !important;
333
+ }
334
+
335
+ .session-status-container .markdown {
336
+ margin: 0 !important;
337
+ padding: 0 !important;
338
+ font-size: 0.85rem !important;
339
+ line-height: 1.4 !important;
340
+ overflow-wrap: break-word !important;
341
+ word-break: break-word !important;
342
+ }
343
+
344
+ @media (max-width: 1200px) {
345
+ .main-layout {
346
+ flex-direction: column !important;
347
+ }
348
+
349
+ .fixed-sidebar {
350
+ width: 100% !important;
351
+ min-width: 100% !important;
352
+ max-width: 100% !important;
353
+ position: static !important;
354
+ }
355
+ }
356
+ """
357
+
358
+ # Create a light theme with explicit light mode settings
359
+ light_theme = gr.themes.Soft( # type: ignore
360
+ primary_hue="green",
361
+ secondary_hue="blue",
362
+ neutral_hue="slate"
363
+ ).set(
364
+ # Force light mode colors
365
+ body_background_fill="white",
366
+ body_text_color="#2d3748",
367
+ background_fill_primary="white",
368
+ background_fill_secondary="#f8f9fa",
369
+ border_color_primary="#d1d5db",
370
+ border_color_accent="#659435",
371
+ button_primary_background_fill="#659435",
372
+ button_primary_text_color="white",
373
+ button_secondary_background_fill="#f8f9fa",
374
+ button_secondary_text_color="#2d3748"
375
+ )
376
+
377
+ with gr.Blocks(
378
+ title="πŸ₯¬ KaLLaM - Thai Motivational Therapeutic Advisor",
379
+ theme=light_theme,
380
+ css=custom_css,
381
+ js="""
382
+ function() {
383
+ // Force light mode on load by removing any dark classes and setting light preferences
384
+ document.documentElement.classList.remove('dark');
385
+ document.body.classList.remove('dark');
386
+
387
+ // Set data attributes for light mode
388
+ document.documentElement.setAttribute('data-theme', 'light');
389
+
390
+ // Override any system preferences for dark mode
391
+ const style = document.createElement('style');
392
+ style.textContent = `
393
+ @media (prefers-color-scheme: dark) {
394
+ :root {
395
+ color-scheme: light !important;
396
+ }
397
+ body, .gradio-container {
398
+ background-color: white !important;
399
+ color: #2d3748 !important;
400
+ }
401
+ }
402
+ `;
403
+ document.head.appendChild(style);
404
+ }
405
+ """
406
+ ) as app:
407
+
408
+ # State management - same as simple app
409
+ session_id = gr.State(value="")
410
+
411
+ # Header
412
+ gr.HTML(f"""
413
+ <div class="kallam-header">
414
+ <div style="display: flex; align-items: center; justify-content: flex-start; gap: 2rem; padding: 0 2rem;">
415
+ {CABBAGE_SVG}
416
+ <div style="text-align: left;">
417
+ <h1 style="text-align: left; margin: 0;">KaLLaM</h1>
418
+ <p class="kallam-subtitle" style="text-align: left; margin: 0.5rem 0 0 0;">Thai Motivational Therapeutic Advisor</p>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ """)
423
+
424
+ # Main layout
425
+ with gr.Row(elem_classes=["main-layout"]):
426
+ # Sidebar with enhanced styling
427
+ with gr.Column(scale=1, elem_classes=["fixed-sidebar"]):
428
+ gr.HTML("""
429
+ <div style="text-align: center; padding: 0.5rem 0 1rem 0;">
430
+ <h3 style="color: #659435; margin: 0; font-size: 1.2rem;">Controls</h3>
431
+ <p style="color: #666; margin: 0.25rem 0 0 0; font-size: 0.9rem;">Manage session and health profile</p>
432
+ </div>
433
+ """)
434
+
435
+ with gr.Group():
436
+ new_session_btn = gr.Button("βž• New Session", variant="primary", size="lg", elem_classes=["btn", "btn-primary"])
437
+ health_profile_btn = gr.Button("πŸ‘€ Custom Health Profile", variant="secondary", elem_classes=["btn", "btn-secondary"])
438
+ clear_session_btn = gr.Button("πŸ—‘οΈ Clear Session", variant="secondary", elem_classes=["btn", "btn-secondary"])
439
+
440
+ # Hidden health profile section
441
+ with gr.Column(visible=False) as health_profile_section:
442
+ gr.HTML('<div style="margin: 1rem 0;"><hr style="border: none; border-top: 1px solid #d1d5db;"></div>')
443
+
444
+ health_context = gr.Textbox(
445
+ label="πŸ₯ Patient's Health Information",
446
+ placeholder="e.g., Patient's name, age, medical conditions (high blood pressure, diabetes), current symptoms, medications, lifestyle factors, mental health status...",
447
+ lines=5,
448
+ max_lines=8,
449
+ info="This information helps KaLLaM provide more personalized and relevant health advice. All data is kept confidential within your session."
450
+ )
451
+
452
+ with gr.Row():
453
+ update_profile_btn = gr.Button("πŸ’Ύ Update Health Profile", variant="primary", elem_classes=["btn", "btn-primary"])
454
+ back_btn = gr.Button("βͺ Back", variant="secondary", elem_classes=["btn", "btn-secondary"])
455
+
456
+ gr.HTML('<div style="margin: 1rem 0;"><hr style="border: none; border-top: 1px solid #d1d5db;"></div>')
457
+
458
+ # Session status
459
+ session_status = gr.Markdown(value="πŸ”„ **Initializing...**")
460
+
461
+ # Main chat area
462
+ with gr.Column(scale=3, elem_classes=["main-content"]):
463
+ gr.HTML("""
464
+ <div style="text-align: center; padding: 1rem 0;">
465
+ <h2 style="color: #659435; margin: 0; font-size: 1.5rem;">πŸ’¬ Health Consultation Chat</h2>
466
+ <p style="color: #666; margin: 0.5rem 0 0 0;">Chat with your AI health advisor in Thai or English</p>
467
+ </div>
468
+ """)
469
+
470
+ chatbot = gr.Chatbot(
471
+ label="Chat with KaLLaM",
472
+ height=500,
473
+ show_label=False,
474
+ type="messages",
475
+ elem_classes=["chat-container"]
476
+ )
477
+
478
+ with gr.Row():
479
+ with gr.Column(scale=5):
480
+ msg = gr.Textbox(
481
+ label="Message",
482
+ placeholder="Ask about your health in Thai or English...",
483
+ lines=1,
484
+ max_lines=4,
485
+ show_label=False,
486
+ elem_classes=["chat-container"]
487
+ )
488
+ with gr.Column(scale=1, min_width=120):
489
+ send_btn = gr.Button("➀", variant="primary", size="lg", elem_classes=["btn", "btn-primary"])
490
+
491
+ # Result display
492
+ result_display = gr.Markdown(visible=False)
493
+
494
+ # Footer
495
+ gr.HTML("""
496
+ <div style="
497
+ position: fixed; bottom: 0; left: 0; right: 0;
498
+ background: linear-gradient(135deg, var(--kallam-secondary) 0%, var(--kallam-primary) 100%);
499
+ color: white; padding: 0.75rem 1rem; text-align: center; font-size: 0.8rem;
500
+ box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1); z-index: 1000;
501
+ border-top: 1px solid rgba(255,255,255,0.2);
502
+ ">
503
+ <div style="max-width: 1400px; margin: 0 auto; display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 1.5rem;">
504
+ <span style="font-weight: 600;">Built with ❀️ by:</span>
505
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 0.2rem;">
506
+ <span style="font-weight: 500;">πŸ‘¨β€πŸ’» Nopnatee Trivoravong</span>
507
+ <div style="display: flex; gap: 0.5rem; font-size: 0.75rem;">
508
+ <span>πŸ“§ nopnatee.triv@gmail.com</span>
509
+ <span>β€’</span>
510
+ <a href="https://github.com/Nopnatee" target="_blank" style="color: rgba(255,255,255,0.9); text-decoration: none;">GitHub</a>
511
+ </div>
512
+ </div>
513
+ <span style="color: rgba(255,255,255,0.7);">|</span>
514
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 0.2rem;">
515
+ <span style="font-weight: 500;">πŸ‘¨β€πŸ’» Khamic Srisutrapon</span>
516
+ <div style="display: flex; gap: 0.5rem; font-size: 0.75rem;">
517
+ <span>πŸ“§ khamic.sk@gmail.com</span>
518
+ <span>β€’</span>
519
+ <a href="https://github.com/Khamic672" target="_blank" style="color: rgba(255,255,255,0.9); text-decoration: none;">GitHub</a>
520
+ </div>
521
+ </div>
522
+ <span style="color: rgba(255,255,255,0.7);">|</span>
523
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 0.2rem;">
524
+ <span style="font-weight: 500;">πŸ‘©β€πŸ’» Napas Siripala</span>
525
+ <div style="display: flex; gap: 0.5rem; font-size: 0.75rem;">
526
+ <span>πŸ“§ millynapas@gmail.com</span>
527
+ <span>β€’</span>
528
+ <a href="https://github.com/kaoqueri" target="_blank" style="color: rgba(255,255,255,0.9); text-decoration: none;">GitHub</a>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </div>
533
+ """)
534
+
535
+ # ====== EVENT HANDLERS - Same pattern as simple app ======
536
+
537
+ # Auto-initialize on page load (same as simple app)
538
+ def _init():
539
+ sid, history, _, status, note = start_new_session("")
540
+ return sid, history, status, note
541
+
542
+ app.load(
543
+ fn=_init,
544
+ inputs=None,
545
+ outputs=[session_id, chatbot, session_status, result_display]
546
+ )
547
+
548
+ # New session
549
+ new_session_btn.click(
550
+ fn=lambda: start_new_session(""),
551
+ inputs=None,
552
+ outputs=[session_id, chatbot, msg, session_status, result_display]
553
+ )
554
+
555
+ # Show/hide health profile section
556
+ def show_health_profile():
557
+ return gr.update(visible=True)
558
+
559
+ def hide_health_profile():
560
+ return gr.update(visible=False)
561
+
562
+ health_profile_btn.click(
563
+ fn=show_health_profile,
564
+ outputs=[health_profile_section]
565
+ )
566
+
567
+ back_btn.click(
568
+ fn=hide_health_profile,
569
+ outputs=[health_profile_section]
570
+ )
571
+
572
+ # Update health profile
573
+ update_profile_btn.click(
574
+ fn=update_health_profile,
575
+ inputs=[session_id, health_context],
576
+ outputs=[result_display, session_status]
577
+ ).then(
578
+ fn=hide_health_profile,
579
+ outputs=[health_profile_section]
580
+ )
581
+
582
+ # Send message with lock/unlock pattern (inspired by simple app)
583
+ send_btn.click(
584
+ fn=lock_inputs,
585
+ inputs=None,
586
+ outputs=[send_btn, msg],
587
+ queue=False, # lock applies instantly
588
+ ).then(
589
+ fn=send_message,
590
+ inputs=[msg, chatbot, session_id],
591
+ outputs=[chatbot, msg, session_id, session_status],
592
+ ).then(
593
+ fn=unlock_inputs,
594
+ inputs=None,
595
+ outputs=[send_btn, msg],
596
+ queue=False,
597
+ )
598
+
599
+ # Enter/submit flow: same treatment
600
+ msg.submit(
601
+ fn=lock_inputs,
602
+ inputs=None,
603
+ outputs=[send_btn, msg],
604
+ queue=False,
605
+ ).then(
606
+ fn=send_message,
607
+ inputs=[msg, chatbot, session_id],
608
+ outputs=[chatbot, msg, session_id, session_status],
609
+ ).then(
610
+ fn=unlock_inputs,
611
+ inputs=None,
612
+ outputs=[send_btn, msg],
613
+ queue=False,
614
+ )
615
+
616
+ # Clear session
617
+ clear_session_btn.click(
618
+ fn=clear_session,
619
+ inputs=[session_id],
620
+ outputs=[session_id, chatbot, msg, session_status, result_display]
621
+ )
622
+
623
+ return app
624
+
625
+ def main():
626
+ app = create_app()
627
+ # Resolve bind address and port
628
+ server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
629
+ server_port = int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", 8080)))
630
+
631
+ # Basic health log to confirm listening address
632
+ try:
633
+ hostname = socket.gethostname()
634
+ ip_addr = socket.gethostbyname(hostname)
635
+ except Exception:
636
+ hostname = "unknown"
637
+ ip_addr = "unknown"
638
+
639
+ logger.info(
640
+ "Starting Gradio app | bind=%s:%s | host=%s ip=%s",
641
+ server_name,
642
+ server_port,
643
+ hostname,
644
+ ip_addr,
645
+ )
646
+ logger.info(
647
+ "Env: PORT=%s GRADIO_SERVER_NAME=%s GRADIO_SERVER_PORT=%s",
648
+ os.getenv("PORT"),
649
+ os.getenv("GRADIO_SERVER_NAME"),
650
+ os.getenv("GRADIO_SERVER_PORT"),
651
+ )
652
+ # Secrets presence check (mask values)
653
+ def _mask(v: str | None) -> str:
654
+ if not v:
655
+ return "<missing>"
656
+ return f"set(len={len(v)})"
657
+ logger.info(
658
+ "Secrets: SEA_LION_API_KEY=%s GEMINI_API_KEY=%s",
659
+ _mask(os.getenv("SEA_LION_API_KEY")),
660
+ _mask(os.getenv("GEMINI_API_KEY")),
661
+ )
662
+ app.launch(
663
+ share=True,
664
+ server_name=server_name, # cloud: 0.0.0.0, local: 127.0.0.1
665
+ server_port=server_port, # cloud: $PORT, local: 7860/8080
666
+ debug=False,
667
+ show_error=True,
668
+ inbrowser=True
669
+ )
670
+
671
+ if __name__ == "__main__":
672
+ main()