ZunairaHawwar commited on
Commit
dc34c8e
·
verified ·
1 Parent(s): a818a2d

Upload components.py

Browse files
Files changed (1) hide show
  1. components.py +542 -0
components.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components.py - Enhanced with Rating and Bookmarking Features
2
+ import streamlit.components.v1 as components
3
+ import html
4
+ import uuid
5
+
6
+ def render_response_box(response_text, response_id):
7
+ """Original render response box for backward compatibility"""
8
+ escaped_text = html.escape(response_text)
9
+
10
+ components.html(f"""
11
+ <div class="response-box">
12
+ <div class="response-header">
13
+ 💬 <strong>Response</strong>
14
+ <button class="copy-button" onclick="copyToClipboard('{response_id}')">📋</button>
15
+ </div>
16
+ <div class="response-content" id="{response_id}">
17
+ {escaped_text}
18
+ </div>
19
+ </div>
20
+
21
+ <div id="toast-{response_id}" class="custom-toast">Copied to clipboard ✅</div>
22
+
23
+ <script>
24
+ function copyToClipboard(id) {{
25
+ try {{
26
+ const element = document.getElementById(id);
27
+ if (!element) {{
28
+ console.error('Element not found:', id);
29
+ return;
30
+ }}
31
+
32
+ const text = element.innerText || element.textContent;
33
+
34
+ if (navigator.clipboard && navigator.clipboard.writeText) {{
35
+ navigator.clipboard.writeText(text).then(() => {{
36
+ showToast(id);
37
+ }}).catch(err => {{
38
+ console.error("Clipboard API failed:", err);
39
+ fallbackCopy(text, id);
40
+ }});
41
+ }} else {{
42
+ fallbackCopy(text, id);
43
+ }}
44
+ }} catch (error) {{
45
+ console.error("Copy failed:", error);
46
+ }}
47
+ }}
48
+
49
+ function fallbackCopy(text, id) {{
50
+ try {{
51
+ const textArea = document.createElement("textarea");
52
+ textArea.value = text;
53
+ textArea.style.position = "fixed";
54
+ textArea.style.left = "-999999px";
55
+ textArea.style.top = "-999999px";
56
+ document.body.appendChild(textArea);
57
+ textArea.focus();
58
+ textArea.select();
59
+
60
+ const successful = document.execCommand('copy');
61
+ document.body.removeChild(textArea);
62
+
63
+ if (successful) {{
64
+ showToast(id);
65
+ }} else {{
66
+ console.error("Fallback copy failed");
67
+ }}
68
+ }} catch (error) {{
69
+ console.error("Fallback copy error:", error);
70
+ }}
71
+ }}
72
+
73
+ function showToast(id) {{
74
+ const toast = document.getElementById("toast-" + id);
75
+ if (toast) {{
76
+ toast.classList.add("show");
77
+ setTimeout(() => {{
78
+ toast.classList.remove("show");
79
+ }}, 3000);
80
+ }}
81
+ }}
82
+ </script>
83
+
84
+ <style>
85
+ .response-box {{
86
+ background-color: #f9f9f9;
87
+ border-radius: 12px;
88
+ padding: 16px;
89
+ margin-top: 10px;
90
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
91
+ font-family: 'Inter', sans-serif;
92
+ border: 1px solid #e0e0e0;
93
+ }}
94
+
95
+ .response-header {{
96
+ display: flex;
97
+ justify-content: space-between;
98
+ align-items: center;
99
+ font-size: 18px;
100
+ margin-bottom: 12px;
101
+ color: #333;
102
+ }}
103
+
104
+ .copy-button {{
105
+ background: #e0e0e0;
106
+ border: none;
107
+ border-radius: 6px;
108
+ padding: 6px 12px;
109
+ cursor: pointer;
110
+ font-size: 16px;
111
+ transition: background-color 0.2s ease;
112
+ }}
113
+
114
+ .copy-button:hover {{
115
+ background-color: #d0d0d0;
116
+ }}
117
+
118
+ .response-content {{
119
+ max-height: 240px;
120
+ overflow-y: auto;
121
+ white-space: pre-wrap;
122
+ background-color: #fff;
123
+ padding: 12px;
124
+ border-radius: 8px;
125
+ border: 1px solid #eee;
126
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
127
+ line-height: 1.6;
128
+ color: #333;
129
+ word-break: break-word;
130
+ }}
131
+
132
+ .custom-toast {{
133
+ visibility: hidden;
134
+ min-width: 220px;
135
+ margin-left: -110px;
136
+ background-color: #4CAF50;
137
+ color: #fff;
138
+ text-align: center;
139
+ border-radius: 8px;
140
+ padding: 12px;
141
+ position: fixed;
142
+ z-index: 9999;
143
+ left: 50%;
144
+ bottom: 50px;
145
+ font-size: 16px;
146
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
147
+ transition: all 0.3s ease;
148
+ opacity: 0;
149
+ transform: translateY(30px);
150
+ }}
151
+
152
+ .custom-toast.show {{
153
+ visibility: visible;
154
+ opacity: 1;
155
+ transform: translateY(0);
156
+ }}
157
+ </style>
158
+ """, height=320)
159
+
160
+ def render_enhanced_response_box(response_text, message_id, session_id, is_bookmarked=False, rating=None, show_actions=True):
161
+ """Enhanced response box with rating and bookmarking features"""
162
+ escaped_text = html.escape(response_text)
163
+
164
+ # Rating button states
165
+ thumbs_up_class = "rating-active" if rating == 1 else ""
166
+ thumbs_down_class = "rating-active" if rating == -1 else ""
167
+
168
+ # Bookmark button state
169
+ bookmark_icon = "🔖" if is_bookmarked else "📑"
170
+ bookmark_class = "bookmark-active" if is_bookmarked else ""
171
+
172
+ action_buttons = ""
173
+ if show_actions:
174
+ action_buttons = f"""
175
+ <div class="action-buttons">
176
+ <button class="action-btn rating-btn {thumbs_up_class}"
177
+ onclick="rateMessage('{session_id}', '{message_id}', 1)"
178
+ title="Rate positively">👍</button>
179
+ <button class="action-btn rating-btn {thumbs_down_class}"
180
+ onclick="rateMessage('{session_id}', '{message_id}', -1)"
181
+ title="Rate negatively">👎</button>
182
+ <button class="action-btn bookmark-btn {bookmark_class}"
183
+ onclick="bookmarkMessage('{session_id}', '{message_id}', {str(not is_bookmarked).lower()})"
184
+ title="{'Remove bookmark' if is_bookmarked else 'Bookmark response'}">{bookmark_icon}</button>
185
+ <button class="action-btn copy-button"
186
+ onclick="copyToClipboard('{message_id}')"
187
+ title="Copy to clipboard">📋</button>
188
+ </div>
189
+ """
190
+
191
+ components.html(f"""
192
+ <div class="enhanced-response-box">
193
+ <div class="response-header">
194
+ 💬 <strong>AI Response</strong>
195
+ {action_buttons}
196
+ </div>
197
+ <div class="response-content" id="{message_id}">
198
+ {escaped_text}
199
+ </div>
200
+ </div>
201
+
202
+ <div id="toast-{message_id}" class="custom-toast">Action completed ✅</div>
203
+
204
+ <script>
205
+ function copyToClipboard(id) {{
206
+ try {{
207
+ const element = document.getElementById(id);
208
+ if (!element) return;
209
+
210
+ const text = element.innerText || element.textContent;
211
+
212
+ if (navigator.clipboard && navigator.clipboard.writeText) {{
213
+ navigator.clipboard.writeText(text).then(() => {{
214
+ showToast(id, 'Copied to clipboard ✅');
215
+ }}).catch(err => {{
216
+ fallbackCopy(text, id);
217
+ }});
218
+ }} else {{
219
+ fallbackCopy(text, id);
220
+ }}
221
+ }} catch (error) {{
222
+ console.error("Copy failed:", error);
223
+ }}
224
+ }}
225
+
226
+ function fallbackCopy(text, id) {{
227
+ try {{
228
+ const textArea = document.createElement("textarea");
229
+ textArea.value = text;
230
+ textArea.style.position = "fixed";
231
+ textArea.style.left = "-999999px";
232
+ textArea.style.top = "-999999px";
233
+ document.body.appendChild(textArea);
234
+ textArea.focus();
235
+ textArea.select();
236
+
237
+ const successful = document.execCommand('copy');
238
+ document.body.removeChild(textArea);
239
+
240
+ if (successful) {{
241
+ showToast(id, 'Copied to clipboard ✅');
242
+ }}
243
+ }} catch (error) {{
244
+ console.error("Fallback copy error:", error);
245
+ }}
246
+ }}
247
+
248
+ function rateMessage(sessionId, messageId, rating) {{
249
+ // Send rating to Streamlit backend
250
+ const data = {{
251
+ action: 'rate_message',
252
+ session_id: sessionId,
253
+ message_id: messageId,
254
+ rating: rating
255
+ }};
256
+
257
+ // Use Streamlit's component communication
258
+ window.parent.postMessage({{
259
+ type: 'streamlit:componentValue',
260
+ value: data
261
+ }}, '*');
262
+
263
+ // Update UI immediately
264
+ const buttons = document.querySelectorAll(`[onclick*="${{messageId}}"]`);
265
+ buttons.forEach(btn => {{
266
+ if (btn.textContent.includes('👍')) {{
267
+ btn.classList.toggle('rating-active', rating === 1);
268
+ }} else if (btn.textContent.includes('👎')) {{
269
+ btn.classList.toggle('rating-active', rating === -1);
270
+ }}
271
+ }});
272
+
273
+ showToast(messageId, rating === 1 ? 'Rated positively 👍' : 'Rated negatively 👎');
274
+ }}
275
+
276
+ function bookmarkMessage(sessionId, messageId, isBookmarked) {{
277
+ // Send bookmark action to Streamlit backend
278
+ const data = {{
279
+ action: 'bookmark_message',
280
+ session_id: sessionId,
281
+ message_id: messageId,
282
+ is_bookmarked: isBookmarked
283
+ }};
284
+
285
+ window.parent.postMessage({{
286
+ type: 'streamlit:componentValue',
287
+ value: data
288
+ }}, '*');
289
+
290
+ // Update UI immediately
291
+ const bookmarkBtn = document.querySelector(`[onclick*="bookmarkMessage('${{sessionId}}', '${{messageId}}'"]`);
292
+ if (bookmarkBtn) {{
293
+ bookmarkBtn.textContent = isBookmarked ? '🔖' : '📑';
294
+ bookmarkBtn.classList.toggle('bookmark-active', isBookmarked);
295
+ bookmarkBtn.setAttribute('onclick', `bookmarkMessage('${{sessionId}}', '${{messageId}}', ${{!isBookmarked}})`);
296
+ bookmarkBtn.title = isBookmarked ? 'Remove bookmark' : 'Bookmark response';
297
+ }}
298
+
299
+ showToast(messageId, isBookmarked ? 'Response bookmarked 🔖' : 'Bookmark removed 📑');
300
+ }}
301
+
302
+ function showToast(id, message) {{
303
+ const toast = document.getElementById("toast-" + id);
304
+ if (toast) {{
305
+ toast.textContent = message;
306
+ toast.classList.add("show");
307
+ setTimeout(() => {{
308
+ toast.classList.remove("show");
309
+ }}, 3000);
310
+ }}
311
+ }}
312
+ </script>
313
+
314
+ <style>
315
+ .enhanced-response-box {{
316
+ background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
317
+ border-radius: 16px;
318
+ padding: 20px;
319
+ margin: 16px 0;
320
+ box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1);
321
+ border: 1px solid #e8eaff;
322
+ font-family: 'Inter', sans-serif;
323
+ position: relative;
324
+ }}
325
+
326
+ .response-header {{
327
+ display: flex;
328
+ justify-content: space-between;
329
+ align-items: center;
330
+ margin-bottom: 16px;
331
+ color: #2d3748;
332
+ font-weight: 600;
333
+ }}
334
+
335
+ .action-buttons {{
336
+ display: flex;
337
+ gap: 8px;
338
+ align-items: center;
339
+ }}
340
+
341
+ .action-btn {{
342
+ background: rgba(255, 255, 255, 0.8);
343
+ border: 1px solid #e2e8f0;
344
+ border-radius: 8px;
345
+ padding: 8px 12px;
346
+ cursor: pointer;
347
+ font-size: 16px;
348
+ transition: all 0.2s ease;
349
+ backdrop-filter: blur(10px);
350
+ min-width: 40px;
351
+ height: 40px;
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ }}
356
+
357
+ .action-btn:hover {{
358
+ background: rgba(255, 255, 255, 0.95);
359
+ border-color: #cbd5e0;
360
+ transform: translateY(-1px);
361
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
362
+ }}
363
+
364
+ .rating-btn.rating-active {{
365
+ background: #667eea;
366
+ color: white;
367
+ border-color: #667eea;
368
+ }}
369
+
370
+ .bookmark-btn.bookmark-active {{
371
+ background: #f6ad55;
372
+ color: white;
373
+ border-color: #f6ad55;
374
+ }}
375
+
376
+ .copy-button:hover {{
377
+ background: #e2e8f0;
378
+ }}
379
+
380
+ .response-content {{
381
+ background: rgba(255, 255, 255, 0.9);
382
+ padding: 20px;
383
+ border-radius: 12px;
384
+ border: 1px solid #e8eaff;
385
+ max-height: 300px;
386
+ overflow-y: auto;
387
+ white-space: pre-wrap;
388
+ font-family: 'Segoe UI', system-ui, sans-serif;
389
+ line-height: 1.7;
390
+ color: #2d3748;
391
+ word-break: break-word;
392
+ backdrop-filter: blur(5px);
393
+ }}
394
+
395
+ .response-content::-webkit-scrollbar {{
396
+ width: 8px;
397
+ }}
398
+
399
+ .response-content::-webkit-scrollbar-track {{
400
+ background: #f7fafc;
401
+ border-radius: 4px;
402
+ }}
403
+
404
+ .response-content::-webkit-scrollbar-thumb {{
405
+ background: linear-gradient(135deg, #667eea, #764ba2);
406
+ border-radius: 4px;
407
+ }}
408
+
409
+ .response-content::-webkit-scrollbar-thumb:hover {{
410
+ background: linear-gradient(135deg, #5a6fd8, #6b46a3);
411
+ }}
412
+
413
+ .custom-toast {{
414
+ visibility: hidden;
415
+ min-width: 250px;
416
+ margin-left: -125px;
417
+ background: linear-gradient(135deg, #667eea, #764ba2);
418
+ color: #fff;
419
+ text-align: center;
420
+ border-radius: 12px;
421
+ padding: 16px 20px;
422
+ position: fixed;
423
+ z-index: 9999;
424
+ left: 50%;
425
+ bottom: 50px;
426
+ font-size: 16px;
427
+ font-weight: 500;
428
+ box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
429
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
430
+ opacity: 0;
431
+ transform: translateY(30px) scale(0.9);
432
+ backdrop-filter: blur(10px);
433
+ }}
434
+
435
+ .custom-toast.show {{
436
+ visibility: visible;
437
+ opacity: 1;
438
+ transform: translateY(0) scale(1);
439
+ }}
440
+
441
+ /* Responsive design */
442
+ @media (max-width: 768px) {{
443
+ .enhanced-response-box {{
444
+ padding: 16px;
445
+ margin: 12px 0;
446
+ }}
447
+
448
+ .action-buttons {{
449
+ gap: 6px;
450
+ }}
451
+
452
+ .action-btn {{
453
+ padding: 6px 8px;
454
+ font-size: 14px;
455
+ min-width: 36px;
456
+ height: 36px;
457
+ }}
458
+
459
+ .response-content {{
460
+ padding: 16px;
461
+ max-height: 250px;
462
+ }}
463
+ }}
464
+
465
+ /* Animation for new responses */
466
+ @keyframes slideInUp {{
467
+ from {{
468
+ opacity: 0;
469
+ transform: translateY(20px);
470
+ }}
471
+ to {{
472
+ opacity: 1;
473
+ transform: translateY(0);
474
+ }}
475
+ }}
476
+
477
+ .enhanced-response-box {{
478
+ animation: slideInUp 0.4s ease-out;
479
+ }}
480
+ </style>
481
+ """, height=400)
482
+
483
+ def render_typing_animation(text, response_id):
484
+ """Render typing animation for the response"""
485
+ escaped_text = html.escape(text)
486
+
487
+ return f'''
488
+ <div class="enhanced-response-box typing-animation">
489
+ <div class="response-header">
490
+ 💬 <strong>AI Response</strong>
491
+ <span class="typing-indicator">
492
+ <span class="typing-dot"></span>
493
+ <span class="typing-dot"></span>
494
+ <span class="typing-dot"></span>
495
+ </span>
496
+ </div>
497
+ <div class="response-content">
498
+ {escaped_text}<span class="cursor">▌</span>
499
+ </div>
500
+ </div>
501
+ <style>
502
+ .typing-indicator {{
503
+ display: flex;
504
+ align-items: center;
505
+ gap: 4px;
506
+ }}
507
+
508
+ .typing-dot {{
509
+ width: 6px;
510
+ height: 6px;
511
+ background: #667eea;
512
+ border-radius: 50%;
513
+ animation: typing-bounce 1.4s infinite ease-in-out;
514
+ }}
515
+
516
+ .typing-dot:nth-child(1) {{ animation-delay: -0.32s; }}
517
+ .typing-dot:nth-child(2) {{ animation-delay: -0.16s; }}
518
+ .typing-dot:nth-child(3) {{ animation-delay: 0s; }}
519
+
520
+ @keyframes typing-bounce {{
521
+ 0%, 80%, 100% {{
522
+ transform: scale(0.8);
523
+ opacity: 0.5;
524
+ }}
525
+ 40% {{
526
+ transform: scale(1);
527
+ opacity: 1;
528
+ }}
529
+ }}
530
+
531
+ .cursor {{
532
+ animation: blink 1s infinite;
533
+ color: #667eea;
534
+ font-weight: bold;
535
+ }}
536
+
537
+ @keyframes blink {{
538
+ 0%, 50% {{ opacity: 1; }}
539
+ 51%, 100% {{ opacity: 0; }}
540
+ }}
541
+ </style>
542
+ '''