anhkhoiphan commited on
Commit
343ad5e
·
verified ·
1 Parent(s): deca6e2

Rename chainlit_demo.py to app.py

Browse files
Files changed (1) hide show
  1. chainlit_demo.py → app.py +418 -418
chainlit_demo.py → app.py RENAMED
@@ -1,419 +1,419 @@
1
- import re
2
- import time
3
- import chainlit as cl
4
- import pandas as pd
5
- import requests
6
- import asyncio
7
- from typing import Dict, Any, Optional, Callable
8
- from dataclasses import dataclass, field
9
-
10
-
11
- API_BASE_URL = "http://127.0.0.1:7861"
12
-
13
-
14
- @dataclass
15
- class ConversationState:
16
- """Data class to hold conversation state"""
17
- specs_advantages: Dict[str, Any] = field(default_factory=dict)
18
- raw_documents: Optional[Dict[str, Any]] = None
19
- outputs: Optional[Dict[str, Any]] = None
20
- selected_model: str = "Gemini 2.0 Flash"
21
-
22
- def reset(self):
23
- """Reset state to initial values"""
24
- self.specs_advantages = {}
25
- self.raw_documents = None
26
- self.outputs = None
27
- self.selected_model = "Gemini 2.0 Flash"
28
-
29
-
30
- class StateManager:
31
- """Manages conversation state operations"""
32
-
33
- @staticmethod
34
- def create_initial_state() -> ConversationState:
35
- """Create initial conversation state"""
36
- return ConversationState()
37
-
38
- @staticmethod
39
- async def clear_chat_state(state: ConversationState):
40
- """Clear all conversation history and reset state via API"""
41
- try:
42
- requests.post(f"{API_BASE_URL}/clear_memory",
43
- json={"reset_cache": True, "reset_model": False})
44
- except Exception as e:
45
- print(f"Warning: clear_memory failed: {e}")
46
-
47
- # Reset state
48
- state.reset()
49
-
50
- @staticmethod
51
- def change_model(state: ConversationState, model_name: str):
52
- """Change the selected model"""
53
- try:
54
- requests.post(f"{API_BASE_URL}/set_model",
55
- json={"model_name": model_name})
56
- state.selected_model = model_name
57
- except Exception as e:
58
- print(f"Warning: set_model failed: {e}")
59
-
60
-
61
- class ChatService:
62
- """Handles chat-related operations"""
63
-
64
- @staticmethod
65
- async def respond_to_chat(
66
- state: ConversationState,
67
- message: str,
68
- image_path: Optional[str] = None
69
- ) -> str:
70
- """Handle chat responses with image support"""
71
- print(f"🔄 === DEBUG STATE ===\n Chat request with model: {state.selected_model}")
72
- start = time.perf_counter()
73
-
74
- # Call API
75
- try:
76
- if image_path:
77
- with open(image_path, 'rb') as f:
78
- files = {"image": f}
79
- data = {"message": message}
80
- resp = requests.post(
81
- f"{API_BASE_URL}/chat_with_image", files=files, data=data, timeout=120)
82
- else:
83
- payload = {"message": message}
84
- resp = requests.post(f"{API_BASE_URL}/chat",
85
- json=payload, timeout=120)
86
-
87
- if resp.status_code == 200:
88
- j = resp.json()
89
- response = j.get("response", "")
90
- specs_advantages = j.get("specs_advantages")
91
- raw_documents = j.get("raw_documents")
92
- outputs = j.get("outputs")
93
- else:
94
- response = f"Error: API status {resp.status_code}"
95
- specs_advantages, raw_documents, outputs = None, None, None
96
- except Exception as e:
97
- response = f"Error calling API: {e}"
98
- specs_advantages, raw_documents, outputs = None, None, None
99
-
100
- end = time.perf_counter()
101
-
102
- # Update state
103
- if isinstance(specs_advantages, dict):
104
- state.specs_advantages = specs_advantages
105
- state.raw_documents = raw_documents
106
- state.outputs = outputs
107
-
108
- return response + f"\n\n*Thời gian xử lí: {end - start:.6f}s*"
109
-
110
-
111
- class DisplayService:
112
- """Handles display-related operations"""
113
-
114
- @staticmethod
115
- def show_specs(state: ConversationState) -> str:
116
- """Generate specifications table"""
117
- specs_map = state.specs_advantages
118
- columns = ["Thông số"]
119
- raw_data = []
120
-
121
- if not specs_map:
122
- return "📄 **Thông số kỹ thuật**\n\nKhông có thông số kỹ thuật nào."
123
-
124
- print(specs_map)
125
- for prod_id, data in specs_map.items():
126
- spec = data.get("specification", None)
127
- model = data.get("model", "")
128
- url = data.get("url", "")
129
-
130
- # Handle both products and solution packages
131
- if url:
132
- full_name = f"**[{data['name']} {model}]({url})**"
133
- else:
134
- full_name = f"**{data['name']} {model}**"
135
-
136
- if full_name not in columns:
137
- columns.append(full_name)
138
-
139
- if spec:
140
- # Check if this is a solution package (contains markdown table)
141
- if "### 📦" in spec:
142
- # For solution packages, parse the markdown table properly
143
- lines = spec.split('\n')
144
- in_table = False
145
- headers = []
146
-
147
- for line in lines:
148
- line = line.strip()
149
- if '|' in line and '---' not in line and line.startswith('|') and line.endswith('|'):
150
- cells = [cell.strip()
151
- for cell in line.split('|')[1:-1]]
152
-
153
- if not in_table:
154
- # This is the header row
155
- headers = cells
156
- in_table = True
157
- continue
158
-
159
- # This is a data row
160
- if len(cells) >= len(headers):
161
- for i, header in enumerate(headers):
162
- if i < len(cells):
163
- param_name = header
164
- param_value = cells[i]
165
-
166
- existing_row = None
167
- for row in raw_data:
168
- if row["Thông số"] == param_name:
169
- existing_row = row
170
- break
171
-
172
- if existing_row:
173
- existing_row[full_name] = param_value
174
- else:
175
- new_row = {"Thông số": param_name}
176
- for col in columns[1:]:
177
- new_row[col] = ""
178
- new_row[full_name] = param_value
179
- raw_data.append(new_row)
180
- elif in_table and (not line or not line.startswith('|')):
181
- in_table = False
182
- else:
183
- # For products, parse specification items
184
- items = re.split(r';|\n', spec)
185
- for item in items:
186
- if ":" in item:
187
- key, value = item.split(':', 1)
188
- spec_key = key.strip().capitalize()
189
- if spec_key == "Vậtl iệu":
190
- spec_key = "Vật liệu"
191
-
192
- existing_row = None
193
- for row in raw_data:
194
- if row["Thông số"] == spec_key:
195
- existing_row = row
196
- break
197
-
198
- if existing_row:
199
- existing_row[full_name] = value.strip() if value else ""
200
- else:
201
- new_row = {"Thông số": spec_key}
202
- for col in columns[1:]:
203
- new_row[col] = ""
204
- new_row[full_name] = value.strip() if value else ""
205
- raw_data.append(new_row)
206
-
207
- if raw_data:
208
- df = pd.DataFrame(raw_data, columns=columns)
209
- df = df.fillna("").replace("None", "").replace("nan", "")
210
- else:
211
- df = pd.DataFrame(
212
- [["Không có thông số kỹ thuật", "", ""]], columns=columns)
213
-
214
- markdown_table = df.to_markdown(index=False)
215
- return f"📄 **Thông số kỹ thuật**\n\n{markdown_table}"
216
-
217
- @staticmethod
218
- def show_advantages(state: ConversationState) -> str:
219
- """Generate advantages table"""
220
- specs_map = state.specs_advantages
221
- columns = ["Tên", "Ưu điểm nổi trội"]
222
- table_data = []
223
-
224
- if not specs_map:
225
- return "💡 **Ưu điểm nổi trội**\n\nKhông có ưu điểm nào."
226
-
227
- for prod_id, data in specs_map.items():
228
- adv = data.get("advantages", "Không có ưu điểm")
229
- model = data.get("model", "")
230
- url = data.get("url", "")
231
-
232
- # Handle both products and solution packages
233
- if url:
234
- full_name = f"**[{data['name']} {model}]({url})**"
235
- else:
236
- full_name = f"**{data['name']} {model}**"
237
-
238
- if adv not in ["Không có ưu điểm", "", None]:
239
- table_data.append([full_name, adv])
240
-
241
- if table_data:
242
- df = pd.DataFrame(table_data, columns=columns)
243
- else:
244
- df = pd.DataFrame([["Không có ưu điểm", ""]], columns=columns)
245
-
246
- markdown_table = df.to_markdown(index=False)
247
- return f"💡 **Ưu điểm nổi trội**\n\n{markdown_table}"
248
-
249
- @staticmethod
250
- def show_solution_packages(state: ConversationState) -> str:
251
- """Show solution packages in a structured format"""
252
- specs_map = state.specs_advantages
253
-
254
- if not specs_map:
255
- return "📦 **Gói sản phẩm**\n\nKhông có gói sản phẩm nào"
256
-
257
- # Filter only solution packages
258
- solution_packages = {}
259
- for key, data in specs_map.items():
260
- if data.get("model") == "Gói giải pháp":
261
- solution_packages[key] = data
262
-
263
- if not solution_packages:
264
- return "📦 **Gói sản phẩm**\n\nKhông có gói sản phẩm nào"
265
-
266
- # Build markdown content for each package
267
- markdown_content = "## 📦 Gói sản phẩm\n\n"
268
-
269
- for key, data in solution_packages.items():
270
- spec_content = data.get("specification", "")
271
- markdown_content += spec_content + "\n\n"
272
-
273
- return markdown_content
274
-
275
-
276
- class UIService:
277
- """Handles UI-related operations"""
278
-
279
- @staticmethod
280
- def create_action_buttons():
281
- """Create persistent action buttons"""
282
- return [
283
- cl.Action(name="show_specs", value="specs", label="📄 Thông số kỹ thuật", payload={"action": "specs"}),
284
- cl.Action(name="show_advantages", value="advantages", label="💡 Ưu điểm nổi trội", payload={"action": "advantages"}),
285
- cl.Action(name="show_packages", value="packages", label="📦 Gói sản phẩm", payload={"action": "packages"}),
286
- cl.Action(name="change_model", value="model", label="🔄 Đổi model", payload={"action": "model"}),
287
- ]
288
-
289
- @staticmethod
290
- async def send_message_with_buttons(content: str, actions=None):
291
- """Send message with optional action buttons"""
292
- if actions is None:
293
- actions = UIService.create_action_buttons()
294
- await cl.Message(content=content, actions=actions).send()
295
-
296
- @staticmethod
297
- async def create_typing_animation():
298
- """Create typing animation effect"""
299
- msg = cl.Message(content="")
300
- await msg.send()
301
-
302
- # Typing animation frames
303
- typing_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
304
-
305
- for i in range(8): # Show animation for ~2 seconds
306
- frame = typing_frames[i % len(typing_frames)]
307
- msg.content = f"{frame} Đang suy nghĩ..."
308
- await msg.update()
309
- await asyncio.sleep(0.25)
310
-
311
- return msg
312
-
313
-
314
- # Application state - using dependency injection pattern
315
- app_state = StateManager.create_initial_state()
316
-
317
-
318
- @cl.on_chat_start
319
- async def on_chat_start():
320
- """Initialize the chat session"""
321
- await StateManager.clear_chat_state(app_state)
322
- await cl.Message(
323
- content="🛍️ **RangDong Sales Agent**\n\nXin chào! Tôi có thể giúp bạn tìm kiếm và tư vấn sản phẩm RangDong. Hãy thử các câu hỏi mẫu:\n\n- Tìm sản phẩm bình giữ nhiệt dung tích dưới 2 lít\n- Tìm sản phẩm ổ cắm thông minh\n- Tư vấn cho tôi đèn học chống cận cho con gái của tôi học lớp 6"
324
- ).send()
325
-
326
- # Create initial action buttons
327
- actions = UIService.create_action_buttons()
328
- await cl.Message(content="Sử dụng các nút bên dưới để:", actions=actions).send()
329
-
330
-
331
- @cl.action_callback("show_specs")
332
- async def on_show_specs(action):
333
- """Handle show specifications action"""
334
- specs_content = DisplayService.show_specs(app_state)
335
- await UIService.send_message_with_buttons(specs_content)
336
-
337
-
338
- @cl.action_callback("show_advantages")
339
- async def on_show_advantages(action):
340
- """Handle show advantages action"""
341
- adv_content = DisplayService.show_advantages(app_state)
342
- await UIService.send_message_with_buttons(adv_content)
343
-
344
-
345
- @cl.action_callback("show_packages")
346
- async def on_show_packages(action):
347
- """Handle show packages action"""
348
- pkg_content = DisplayService.show_solution_packages(app_state)
349
- await UIService.send_message_with_buttons(pkg_content)
350
-
351
-
352
- @cl.action_callback("change_model")
353
- async def on_change_model(action):
354
- """Handle model change action"""
355
- models = ["Gemini 2.0 Flash", "Gemini 2.5 Flash Lite", "Gemini 2.0 Flash Lite"]
356
-
357
- # Create model selection actions
358
- model_actions = [
359
- cl.Action(name=f"select_model_{i}", value=model, label=model, payload={"model": model})
360
- for i, model in enumerate(models)
361
- ]
362
-
363
- # Add back button to return to main actions
364
- model_actions.append(
365
- cl.Action(name="back_to_main", value="back", label="🔙 Quay lại", payload={"action": "back"})
366
- )
367
-
368
- await cl.Message(
369
- content=f"**Model hiện tại**: {app_state.selected_model}\n\nChọn model mới:",
370
- actions=model_actions
371
- ).send()
372
-
373
-
374
- @cl.action_callback("back_to_main")
375
- async def on_back_to_main(action):
376
- """Handle back to main menu action"""
377
- actions = UIService.create_action_buttons()
378
- await cl.Message(content="📋 **Menu chính**\n\nSử dụng các nút bên dưới để:", actions=actions).send()
379
-
380
-
381
- @cl.action_callback("select_model_0")
382
- async def on_select_model_0(action):
383
- StateManager.change_model(app_state, "Gemini 2.0 Flash")
384
- await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash**")
385
-
386
-
387
- @cl.action_callback("select_model_1")
388
- async def on_select_model_1(action):
389
- StateManager.change_model(app_state, "Gemini 2.5 Flash Lite")
390
- await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.5 Flash Lite**")
391
-
392
-
393
- @cl.action_callback("select_model_2")
394
- async def on_select_model_2(action):
395
- StateManager.change_model(app_state, "Gemini 2.0 Flash Lite")
396
- await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash Lite**")
397
-
398
-
399
- @cl.on_message
400
- async def main(message: cl.Message):
401
- """Main message handler"""
402
- # Handle images if present
403
- image_path = None
404
- if message.elements:
405
- for element in message.elements:
406
- if isinstance(element, cl.Image):
407
- image_path = element.path
408
- break
409
-
410
- # Show typing animation
411
- typing_msg = await UIService.create_typing_animation()
412
-
413
- # Get response from API - injecting state as dependency
414
- response = await ChatService.respond_to_chat(app_state, message.content, image_path)
415
-
416
- # Update the typing message with final response and buttons
417
- typing_msg.content = response
418
- typing_msg.actions = UIService.create_action_buttons()
419
  await typing_msg.update()
 
1
+ import re
2
+ import time
3
+ import chainlit as cl
4
+ import pandas as pd
5
+ import requests
6
+ import asyncio
7
+ from typing import Dict, Any, Optional, Callable
8
+ from dataclasses import dataclass, field
9
+
10
+
11
+ API_BASE_URL = "https://sale-agent-m179.onrender.com"
12
+
13
+
14
+ @dataclass
15
+ class ConversationState:
16
+ """Data class to hold conversation state"""
17
+ specs_advantages: Dict[str, Any] = field(default_factory=dict)
18
+ raw_documents: Optional[Dict[str, Any]] = None
19
+ outputs: Optional[Dict[str, Any]] = None
20
+ selected_model: str = "Gemini 2.0 Flash"
21
+
22
+ def reset(self):
23
+ """Reset state to initial values"""
24
+ self.specs_advantages = {}
25
+ self.raw_documents = None
26
+ self.outputs = None
27
+ self.selected_model = "Gemini 2.0 Flash"
28
+
29
+
30
+ class StateManager:
31
+ """Manages conversation state operations"""
32
+
33
+ @staticmethod
34
+ def create_initial_state() -> ConversationState:
35
+ """Create initial conversation state"""
36
+ return ConversationState()
37
+
38
+ @staticmethod
39
+ async def clear_chat_state(state: ConversationState):
40
+ """Clear all conversation history and reset state via API"""
41
+ try:
42
+ requests.post(f"{API_BASE_URL}/clear_memory",
43
+ json={"reset_cache": True, "reset_model": False})
44
+ except Exception as e:
45
+ print(f"Warning: clear_memory failed: {e}")
46
+
47
+ # Reset state
48
+ state.reset()
49
+
50
+ @staticmethod
51
+ def change_model(state: ConversationState, model_name: str):
52
+ """Change the selected model"""
53
+ try:
54
+ requests.post(f"{API_BASE_URL}/set_model",
55
+ json={"model_name": model_name})
56
+ state.selected_model = model_name
57
+ except Exception as e:
58
+ print(f"Warning: set_model failed: {e}")
59
+
60
+
61
+ class ChatService:
62
+ """Handles chat-related operations"""
63
+
64
+ @staticmethod
65
+ async def respond_to_chat(
66
+ state: ConversationState,
67
+ message: str,
68
+ image_path: Optional[str] = None
69
+ ) -> str:
70
+ """Handle chat responses with image support"""
71
+ print(f"🔄 === DEBUG STATE ===\n Chat request with model: {state.selected_model}")
72
+ start = time.perf_counter()
73
+
74
+ # Call API
75
+ try:
76
+ if image_path:
77
+ with open(image_path, 'rb') as f:
78
+ files = {"image": f}
79
+ data = {"message": message}
80
+ resp = requests.post(
81
+ f"{API_BASE_URL}/chat_with_image", files=files, data=data, timeout=120)
82
+ else:
83
+ payload = {"message": message}
84
+ resp = requests.post(f"{API_BASE_URL}/chat",
85
+ json=payload, timeout=120)
86
+
87
+ if resp.status_code == 200:
88
+ j = resp.json()
89
+ response = j.get("response", "")
90
+ specs_advantages = j.get("specs_advantages")
91
+ raw_documents = j.get("raw_documents")
92
+ outputs = j.get("outputs")
93
+ else:
94
+ response = f"Error: API status {resp.status_code}"
95
+ specs_advantages, raw_documents, outputs = None, None, None
96
+ except Exception as e:
97
+ response = f"Error calling API: {e}"
98
+ specs_advantages, raw_documents, outputs = None, None, None
99
+
100
+ end = time.perf_counter()
101
+
102
+ # Update state
103
+ if isinstance(specs_advantages, dict):
104
+ state.specs_advantages = specs_advantages
105
+ state.raw_documents = raw_documents
106
+ state.outputs = outputs
107
+
108
+ return response + f"\n\n*Thời gian xử lí: {end - start:.6f}s*"
109
+
110
+
111
+ class DisplayService:
112
+ """Handles display-related operations"""
113
+
114
+ @staticmethod
115
+ def show_specs(state: ConversationState) -> str:
116
+ """Generate specifications table"""
117
+ specs_map = state.specs_advantages
118
+ columns = ["Thông số"]
119
+ raw_data = []
120
+
121
+ if not specs_map:
122
+ return "📄 **Thông số kỹ thuật**\n\nKhông có thông số kỹ thuật nào."
123
+
124
+ print(specs_map)
125
+ for prod_id, data in specs_map.items():
126
+ spec = data.get("specification", None)
127
+ model = data.get("model", "")
128
+ url = data.get("url", "")
129
+
130
+ # Handle both products and solution packages
131
+ if url:
132
+ full_name = f"**[{data['name']} {model}]({url})**"
133
+ else:
134
+ full_name = f"**{data['name']} {model}**"
135
+
136
+ if full_name not in columns:
137
+ columns.append(full_name)
138
+
139
+ if spec:
140
+ # Check if this is a solution package (contains markdown table)
141
+ if "### 📦" in spec:
142
+ # For solution packages, parse the markdown table properly
143
+ lines = spec.split('\n')
144
+ in_table = False
145
+ headers = []
146
+
147
+ for line in lines:
148
+ line = line.strip()
149
+ if '|' in line and '---' not in line and line.startswith('|') and line.endswith('|'):
150
+ cells = [cell.strip()
151
+ for cell in line.split('|')[1:-1]]
152
+
153
+ if not in_table:
154
+ # This is the header row
155
+ headers = cells
156
+ in_table = True
157
+ continue
158
+
159
+ # This is a data row
160
+ if len(cells) >= len(headers):
161
+ for i, header in enumerate(headers):
162
+ if i < len(cells):
163
+ param_name = header
164
+ param_value = cells[i]
165
+
166
+ existing_row = None
167
+ for row in raw_data:
168
+ if row["Thông số"] == param_name:
169
+ existing_row = row
170
+ break
171
+
172
+ if existing_row:
173
+ existing_row[full_name] = param_value
174
+ else:
175
+ new_row = {"Thông số": param_name}
176
+ for col in columns[1:]:
177
+ new_row[col] = ""
178
+ new_row[full_name] = param_value
179
+ raw_data.append(new_row)
180
+ elif in_table and (not line or not line.startswith('|')):
181
+ in_table = False
182
+ else:
183
+ # For products, parse specification items
184
+ items = re.split(r';|\n', spec)
185
+ for item in items:
186
+ if ":" in item:
187
+ key, value = item.split(':', 1)
188
+ spec_key = key.strip().capitalize()
189
+ if spec_key == "Vậtl iệu":
190
+ spec_key = "Vật liệu"
191
+
192
+ existing_row = None
193
+ for row in raw_data:
194
+ if row["Thông số"] == spec_key:
195
+ existing_row = row
196
+ break
197
+
198
+ if existing_row:
199
+ existing_row[full_name] = value.strip() if value else ""
200
+ else:
201
+ new_row = {"Thông số": spec_key}
202
+ for col in columns[1:]:
203
+ new_row[col] = ""
204
+ new_row[full_name] = value.strip() if value else ""
205
+ raw_data.append(new_row)
206
+
207
+ if raw_data:
208
+ df = pd.DataFrame(raw_data, columns=columns)
209
+ df = df.fillna("").replace("None", "").replace("nan", "")
210
+ else:
211
+ df = pd.DataFrame(
212
+ [["Không có thông số kỹ thuật", "", ""]], columns=columns)
213
+
214
+ markdown_table = df.to_markdown(index=False)
215
+ return f"📄 **Thông số kỹ thuật**\n\n{markdown_table}"
216
+
217
+ @staticmethod
218
+ def show_advantages(state: ConversationState) -> str:
219
+ """Generate advantages table"""
220
+ specs_map = state.specs_advantages
221
+ columns = ["Tên", "Ưu điểm nổi trội"]
222
+ table_data = []
223
+
224
+ if not specs_map:
225
+ return "💡 **Ưu điểm nổi trội**\n\nKhông có ưu điểm nào."
226
+
227
+ for prod_id, data in specs_map.items():
228
+ adv = data.get("advantages", "Không có ưu điểm")
229
+ model = data.get("model", "")
230
+ url = data.get("url", "")
231
+
232
+ # Handle both products and solution packages
233
+ if url:
234
+ full_name = f"**[{data['name']} {model}]({url})**"
235
+ else:
236
+ full_name = f"**{data['name']} {model}**"
237
+
238
+ if adv not in ["Không có ưu điểm", "", None]:
239
+ table_data.append([full_name, adv])
240
+
241
+ if table_data:
242
+ df = pd.DataFrame(table_data, columns=columns)
243
+ else:
244
+ df = pd.DataFrame([["Không có ưu điểm", ""]], columns=columns)
245
+
246
+ markdown_table = df.to_markdown(index=False)
247
+ return f"💡 **Ưu điểm nổi trội**\n\n{markdown_table}"
248
+
249
+ @staticmethod
250
+ def show_solution_packages(state: ConversationState) -> str:
251
+ """Show solution packages in a structured format"""
252
+ specs_map = state.specs_advantages
253
+
254
+ if not specs_map:
255
+ return "📦 **Gói sản phẩm**\n\nKhông có gói sản phẩm nào"
256
+
257
+ # Filter only solution packages
258
+ solution_packages = {}
259
+ for key, data in specs_map.items():
260
+ if data.get("model") == "Gói giải pháp":
261
+ solution_packages[key] = data
262
+
263
+ if not solution_packages:
264
+ return "📦 **Gói sản phẩm**\n\nKhông có gói sản phẩm nào"
265
+
266
+ # Build markdown content for each package
267
+ markdown_content = "## 📦 Gói sản phẩm\n\n"
268
+
269
+ for key, data in solution_packages.items():
270
+ spec_content = data.get("specification", "")
271
+ markdown_content += spec_content + "\n\n"
272
+
273
+ return markdown_content
274
+
275
+
276
+ class UIService:
277
+ """Handles UI-related operations"""
278
+
279
+ @staticmethod
280
+ def create_action_buttons():
281
+ """Create persistent action buttons"""
282
+ return [
283
+ cl.Action(name="show_specs", value="specs", label="📄 Thông số kỹ thuật", payload={"action": "specs"}),
284
+ cl.Action(name="show_advantages", value="advantages", label="💡 Ưu điểm nổi trội", payload={"action": "advantages"}),
285
+ cl.Action(name="show_packages", value="packages", label="📦 Gói sản phẩm", payload={"action": "packages"}),
286
+ cl.Action(name="change_model", value="model", label="🔄 Đổi model", payload={"action": "model"}),
287
+ ]
288
+
289
+ @staticmethod
290
+ async def send_message_with_buttons(content: str, actions=None):
291
+ """Send message with optional action buttons"""
292
+ if actions is None:
293
+ actions = UIService.create_action_buttons()
294
+ await cl.Message(content=content, actions=actions).send()
295
+
296
+ @staticmethod
297
+ async def create_typing_animation():
298
+ """Create typing animation effect"""
299
+ msg = cl.Message(content="")
300
+ await msg.send()
301
+
302
+ # Typing animation frames
303
+ typing_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
304
+
305
+ for i in range(8): # Show animation for ~2 seconds
306
+ frame = typing_frames[i % len(typing_frames)]
307
+ msg.content = f"{frame} Đang suy nghĩ..."
308
+ await msg.update()
309
+ await asyncio.sleep(0.25)
310
+
311
+ return msg
312
+
313
+
314
+ # Application state - using dependency injection pattern
315
+ app_state = StateManager.create_initial_state()
316
+
317
+
318
+ @cl.on_chat_start
319
+ async def on_chat_start():
320
+ """Initialize the chat session"""
321
+ await StateManager.clear_chat_state(app_state)
322
+ await cl.Message(
323
+ content="🛍️ **RangDong Sales Agent**\n\nXin chào! Tôi có thể giúp bạn tìm kiếm và tư vấn sản phẩm RangDong. Hãy thử các câu hỏi mẫu:\n\n- Tìm sản phẩm bình giữ nhiệt dung tích dưới 2 lít\n- Tìm sản phẩm ổ cắm thông minh\n- Tư vấn cho tôi đèn học chống cận cho con gái của tôi học lớp 6"
324
+ ).send()
325
+
326
+ # Create initial action buttons
327
+ actions = UIService.create_action_buttons()
328
+ await cl.Message(content="Sử dụng các nút bên dưới để:", actions=actions).send()
329
+
330
+
331
+ @cl.action_callback("show_specs")
332
+ async def on_show_specs(action):
333
+ """Handle show specifications action"""
334
+ specs_content = DisplayService.show_specs(app_state)
335
+ await UIService.send_message_with_buttons(specs_content)
336
+
337
+
338
+ @cl.action_callback("show_advantages")
339
+ async def on_show_advantages(action):
340
+ """Handle show advantages action"""
341
+ adv_content = DisplayService.show_advantages(app_state)
342
+ await UIService.send_message_with_buttons(adv_content)
343
+
344
+
345
+ @cl.action_callback("show_packages")
346
+ async def on_show_packages(action):
347
+ """Handle show packages action"""
348
+ pkg_content = DisplayService.show_solution_packages(app_state)
349
+ await UIService.send_message_with_buttons(pkg_content)
350
+
351
+
352
+ @cl.action_callback("change_model")
353
+ async def on_change_model(action):
354
+ """Handle model change action"""
355
+ models = ["Gemini 2.0 Flash", "Gemini 2.5 Flash Lite", "Gemini 2.0 Flash Lite"]
356
+
357
+ # Create model selection actions
358
+ model_actions = [
359
+ cl.Action(name=f"select_model_{i}", value=model, label=model, payload={"model": model})
360
+ for i, model in enumerate(models)
361
+ ]
362
+
363
+ # Add back button to return to main actions
364
+ model_actions.append(
365
+ cl.Action(name="back_to_main", value="back", label="🔙 Quay lại", payload={"action": "back"})
366
+ )
367
+
368
+ await cl.Message(
369
+ content=f"**Model hiện tại**: {app_state.selected_model}\n\nChọn model mới:",
370
+ actions=model_actions
371
+ ).send()
372
+
373
+
374
+ @cl.action_callback("back_to_main")
375
+ async def on_back_to_main(action):
376
+ """Handle back to main menu action"""
377
+ actions = UIService.create_action_buttons()
378
+ await cl.Message(content="📋 **Menu chính**\n\nSử dụng các nút bên dưới để:", actions=actions).send()
379
+
380
+
381
+ @cl.action_callback("select_model_0")
382
+ async def on_select_model_0(action):
383
+ StateManager.change_model(app_state, "Gemini 2.0 Flash")
384
+ await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash**")
385
+
386
+
387
+ @cl.action_callback("select_model_1")
388
+ async def on_select_model_1(action):
389
+ StateManager.change_model(app_state, "Gemini 2.5 Flash Lite")
390
+ await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.5 Flash Lite**")
391
+
392
+
393
+ @cl.action_callback("select_model_2")
394
+ async def on_select_model_2(action):
395
+ StateManager.change_model(app_state, "Gemini 2.0 Flash Lite")
396
+ await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash Lite**")
397
+
398
+
399
+ @cl.on_message
400
+ async def main(message: cl.Message):
401
+ """Main message handler"""
402
+ # Handle images if present
403
+ image_path = None
404
+ if message.elements:
405
+ for element in message.elements:
406
+ if isinstance(element, cl.Image):
407
+ image_path = element.path
408
+ break
409
+
410
+ # Show typing animation
411
+ typing_msg = await UIService.create_typing_animation()
412
+
413
+ # Get response from API - injecting state as dependency
414
+ response = await ChatService.respond_to_chat(app_state, message.content, image_path)
415
+
416
+ # Update the typing message with final response and buttons
417
+ typing_msg.content = response
418
+ typing_msg.actions = UIService.create_action_buttons()
419
  await typing_msg.update()