Spaces:
Sleeping
Sleeping
| # components.py - Enhanced with Rating and Bookmarking Features | |
| import streamlit.components.v1 as components | |
| import html | |
| import uuid | |
| def render_response_box(response_text, response_id): | |
| """Original render response box for backward compatibility""" | |
| escaped_text = html.escape(response_text) | |
| components.html(f""" | |
| <div class="response-box"> | |
| <div class="response-header"> | |
| π¬ <strong>Response</strong> | |
| <button class="copy-button" onclick="copyToClipboard('{response_id}')">π</button> | |
| </div> | |
| <div class="response-content" id="{response_id}"> | |
| {escaped_text} | |
| </div> | |
| </div> | |
| <div id="toast-{response_id}" class="custom-toast">Copied to clipboard β </div> | |
| <script> | |
| function copyToClipboard(id) {{ | |
| try {{ | |
| const element = document.getElementById(id); | |
| if (!element) {{ | |
| console.error('Element not found:', id); | |
| return; | |
| }} | |
| const text = element.innerText || element.textContent; | |
| if (navigator.clipboard && navigator.clipboard.writeText) {{ | |
| navigator.clipboard.writeText(text).then(() => {{ | |
| showToast(id); | |
| }}).catch(err => {{ | |
| console.error("Clipboard API failed:", err); | |
| fallbackCopy(text, id); | |
| }}); | |
| }} else {{ | |
| fallbackCopy(text, id); | |
| }} | |
| }} catch (error) {{ | |
| console.error("Copy failed:", error); | |
| }} | |
| }} | |
| function fallbackCopy(text, id) {{ | |
| try {{ | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = text; | |
| textArea.style.position = "fixed"; | |
| textArea.style.left = "-999999px"; | |
| textArea.style.top = "-999999px"; | |
| document.body.appendChild(textArea); | |
| textArea.focus(); | |
| textArea.select(); | |
| const successful = document.execCommand('copy'); | |
| document.body.removeChild(textArea); | |
| if (successful) {{ | |
| showToast(id); | |
| }} else {{ | |
| console.error("Fallback copy failed"); | |
| }} | |
| }} catch (error) {{ | |
| console.error("Fallback copy error:", error); | |
| }} | |
| }} | |
| function showToast(id) {{ | |
| const toast = document.getElementById("toast-" + id); | |
| if (toast) {{ | |
| toast.classList.add("show"); | |
| setTimeout(() => {{ | |
| toast.classList.remove("show"); | |
| }}, 3000); | |
| }} | |
| }} | |
| </script> | |
| <style> | |
| .response-box {{ | |
| background-color: #f9f9f9; | |
| border-radius: 12px; | |
| padding: 16px; | |
| margin-top: 10px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.08); | |
| font-family: 'Inter', sans-serif; | |
| border: 1px solid #e0e0e0; | |
| }} | |
| .response-header {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 18px; | |
| margin-bottom: 12px; | |
| color: #333; | |
| }} | |
| .copy-button {{ | |
| background: #e0e0e0; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 6px 12px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: background-color 0.2s ease; | |
| }} | |
| .copy-button:hover {{ | |
| background-color: #d0d0d0; | |
| }} | |
| .response-content {{ | |
| max-height: 240px; | |
| overflow-y: auto; | |
| white-space: pre-wrap; | |
| background-color: #fff; | |
| padding: 12px; | |
| border-radius: 8px; | |
| border: 1px solid #eee; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| word-break: break-word; | |
| }} | |
| .custom-toast {{ | |
| visibility: hidden; | |
| min-width: 220px; | |
| margin-left: -110px; | |
| background-color: #4CAF50; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 8px; | |
| padding: 12px; | |
| position: fixed; | |
| z-index: 9999; | |
| left: 50%; | |
| bottom: 50px; | |
| font-size: 16px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| transition: all 0.3s ease; | |
| opacity: 0; | |
| transform: translateY(30px); | |
| }} | |
| .custom-toast.show {{ | |
| visibility: visible; | |
| opacity: 1; | |
| transform: translateY(0); | |
| }} | |
| </style> | |
| """, height=320) | |
| def render_enhanced_response_box(response_text, message_id, session_id, is_bookmarked=False, rating=None, show_actions=True): | |
| """Enhanced response box with rating and bookmarking features""" | |
| escaped_text = html.escape(response_text) | |
| # Rating button states | |
| thumbs_up_class = "rating-active" if rating == 1 else "" | |
| thumbs_down_class = "rating-active" if rating == -1 else "" | |
| # Bookmark button state | |
| bookmark_icon = "π" if is_bookmarked else "π" | |
| bookmark_class = "bookmark-active" if is_bookmarked else "" | |
| action_buttons = "" | |
| if show_actions: | |
| action_buttons = f""" | |
| <div class="action-buttons"> | |
| <button class="action-btn rating-btn {thumbs_up_class}" | |
| onclick="rateMessage('{session_id}', '{message_id}', 1)" | |
| title="Rate positively">π</button> | |
| <button class="action-btn rating-btn {thumbs_down_class}" | |
| onclick="rateMessage('{session_id}', '{message_id}', -1)" | |
| title="Rate negatively">π</button> | |
| <button class="action-btn bookmark-btn {bookmark_class}" | |
| onclick="bookmarkMessage('{session_id}', '{message_id}', {str(not is_bookmarked).lower()})" | |
| title="{'Remove bookmark' if is_bookmarked else 'Bookmark response'}">{bookmark_icon}</button> | |
| <button class="action-btn copy-button" | |
| onclick="copyToClipboard('{message_id}')" | |
| title="Copy to clipboard">π</button> | |
| </div> | |
| """ | |
| components.html(f""" | |
| <div class="enhanced-response-box"> | |
| <div class="response-header"> | |
| π¬ <strong>AI Response</strong> | |
| {action_buttons} | |
| </div> | |
| <div class="response-content" id="{message_id}"> | |
| {escaped_text} | |
| </div> | |
| </div> | |
| <div id="toast-{message_id}" class="custom-toast">Action completed β </div> | |
| <script> | |
| function copyToClipboard(id) {{ | |
| try {{ | |
| const element = document.getElementById(id); | |
| if (!element) return; | |
| const text = element.innerText || element.textContent; | |
| if (navigator.clipboard && navigator.clipboard.writeText) {{ | |
| navigator.clipboard.writeText(text).then(() => {{ | |
| showToast(id, 'Copied to clipboard β '); | |
| }}).catch(err => {{ | |
| fallbackCopy(text, id); | |
| }}); | |
| }} else {{ | |
| fallbackCopy(text, id); | |
| }} | |
| }} catch (error) {{ | |
| console.error("Copy failed:", error); | |
| }} | |
| }} | |
| function fallbackCopy(text, id) {{ | |
| try {{ | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = text; | |
| textArea.style.position = "fixed"; | |
| textArea.style.left = "-999999px"; | |
| textArea.style.top = "-999999px"; | |
| document.body.appendChild(textArea); | |
| textArea.focus(); | |
| textArea.select(); | |
| const successful = document.execCommand('copy'); | |
| document.body.removeChild(textArea); | |
| if (successful) {{ | |
| showToast(id, 'Copied to clipboard β '); | |
| }} | |
| }} catch (error) {{ | |
| console.error("Fallback copy error:", error); | |
| }} | |
| }} | |
| function rateMessage(sessionId, messageId, rating) {{ | |
| // Send rating to Streamlit backend | |
| const data = {{ | |
| action: 'rate_message', | |
| session_id: sessionId, | |
| message_id: messageId, | |
| rating: rating | |
| }}; | |
| // Use Streamlit's component communication | |
| window.parent.postMessage({{ | |
| type: 'streamlit:componentValue', | |
| value: data | |
| }}, '*'); | |
| // Update UI immediately | |
| const buttons = document.querySelectorAll(`[onclick*="${{messageId}}"]`); | |
| buttons.forEach(btn => {{ | |
| if (btn.textContent.includes('π')) {{ | |
| btn.classList.toggle('rating-active', rating === 1); | |
| }} else if (btn.textContent.includes('π')) {{ | |
| btn.classList.toggle('rating-active', rating === -1); | |
| }} | |
| }}); | |
| showToast(messageId, rating === 1 ? 'Rated positively π' : 'Rated negatively π'); | |
| }} | |
| function bookmarkMessage(sessionId, messageId, isBookmarked) {{ | |
| // Send bookmark action to Streamlit backend | |
| const data = {{ | |
| action: 'bookmark_message', | |
| session_id: sessionId, | |
| message_id: messageId, | |
| is_bookmarked: isBookmarked | |
| }}; | |
| window.parent.postMessage({{ | |
| type: 'streamlit:componentValue', | |
| value: data | |
| }}, '*'); | |
| // Update UI immediately | |
| const bookmarkBtn = document.querySelector(`[onclick*="bookmarkMessage('${{sessionId}}', '${{messageId}}'"]`); | |
| if (bookmarkBtn) {{ | |
| bookmarkBtn.textContent = isBookmarked ? 'π' : 'π'; | |
| bookmarkBtn.classList.toggle('bookmark-active', isBookmarked); | |
| bookmarkBtn.setAttribute('onclick', `bookmarkMessage('${{sessionId}}', '${{messageId}}', ${{!isBookmarked}})`); | |
| bookmarkBtn.title = isBookmarked ? 'Remove bookmark' : 'Bookmark response'; | |
| }} | |
| showToast(messageId, isBookmarked ? 'Response bookmarked π' : 'Bookmark removed π'); | |
| }} | |
| function showToast(id, message) {{ | |
| const toast = document.getElementById("toast-" + id); | |
| if (toast) {{ | |
| toast.textContent = message; | |
| toast.classList.add("show"); | |
| setTimeout(() => {{ | |
| toast.classList.remove("show"); | |
| }}, 3000); | |
| }} | |
| }} | |
| </script> | |
| <style> | |
| .enhanced-response-box {{ | |
| background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%); | |
| border-radius: 16px; | |
| padding: 20px; | |
| margin: 16px 0; | |
| box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1); | |
| border: 1px solid #e8eaff; | |
| font-family: 'Inter', sans-serif; | |
| position: relative; | |
| }} | |
| .response-header {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| color: #2d3748; | |
| font-weight: 600; | |
| }} | |
| .action-buttons {{ | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| }} | |
| .action-btn {{ | |
| background: rgba(255, 255, 255, 0.8); | |
| border: 1px solid #e2e8f0; | |
| border-radius: 8px; | |
| padding: 8px 12px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: all 0.2s ease; | |
| backdrop-filter: blur(10px); | |
| min-width: 40px; | |
| height: 40px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| }} | |
| .action-btn:hover {{ | |
| background: rgba(255, 255, 255, 0.95); | |
| border-color: #cbd5e0; | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| }} | |
| .rating-btn.rating-active {{ | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| }} | |
| .bookmark-btn.bookmark-active {{ | |
| background: #f6ad55; | |
| color: white; | |
| border-color: #f6ad55; | |
| }} | |
| .copy-button:hover {{ | |
| background: #e2e8f0; | |
| }} | |
| .response-content {{ | |
| background: rgba(255, 255, 255, 0.9); | |
| padding: 20px; | |
| border-radius: 12px; | |
| border: 1px solid #e8eaff; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| white-space: pre-wrap; | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| line-height: 1.7; | |
| color: #2d3748; | |
| word-break: break-word; | |
| backdrop-filter: blur(5px); | |
| }} | |
| .response-content::-webkit-scrollbar {{ | |
| width: 8px; | |
| }} | |
| .response-content::-webkit-scrollbar-track {{ | |
| background: #f7fafc; | |
| border-radius: 4px; | |
| }} | |
| .response-content::-webkit-scrollbar-thumb {{ | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| border-radius: 4px; | |
| }} | |
| .response-content::-webkit-scrollbar-thumb:hover {{ | |
| background: linear-gradient(135deg, #5a6fd8, #6b46a3); | |
| }} | |
| .custom-toast {{ | |
| visibility: hidden; | |
| min-width: 250px; | |
| margin-left: -125px; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 12px; | |
| padding: 16px 20px; | |
| position: fixed; | |
| z-index: 9999; | |
| left: 50%; | |
| bottom: 50px; | |
| font-size: 16px; | |
| font-weight: 500; | |
| box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| opacity: 0; | |
| transform: translateY(30px) scale(0.9); | |
| backdrop-filter: blur(10px); | |
| }} | |
| .custom-toast.show {{ | |
| visibility: visible; | |
| opacity: 1; | |
| transform: translateY(0) scale(1); | |
| }} | |
| /* Responsive design */ | |
| @media (max-width: 768px) {{ | |
| .enhanced-response-box {{ | |
| padding: 16px; | |
| margin: 12px 0; | |
| }} | |
| .action-buttons {{ | |
| gap: 6px; | |
| }} | |
| .action-btn {{ | |
| padding: 6px 8px; | |
| font-size: 14px; | |
| min-width: 36px; | |
| height: 36px; | |
| }} | |
| .response-content {{ | |
| padding: 16px; | |
| max-height: 250px; | |
| }} | |
| }} | |
| /* Animation for new responses */ | |
| @keyframes slideInUp {{ | |
| from {{ | |
| opacity: 0; | |
| transform: translateY(20px); | |
| }} | |
| to {{ | |
| opacity: 1; | |
| transform: translateY(0); | |
| }} | |
| }} | |
| .enhanced-response-box {{ | |
| animation: slideInUp 0.4s ease-out; | |
| }} | |
| </style> | |
| """, height=400) | |
| def render_typing_animation(text, response_id): | |
| """Render typing animation for the response""" | |
| escaped_text = html.escape(text) | |
| return f''' | |
| <div class="enhanced-response-box typing-animation"> | |
| <div class="response-header"> | |
| π¬ <strong>AI Response</strong> | |
| <span class="typing-indicator"> | |
| <span class="typing-dot"></span> | |
| <span class="typing-dot"></span> | |
| <span class="typing-dot"></span> | |
| </span> | |
| </div> | |
| <div class="response-content"> | |
| {escaped_text}<span class="cursor">β</span> | |
| </div> | |
| </div> | |
| <style> | |
| .typing-indicator {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| }} | |
| .typing-dot {{ | |
| width: 6px; | |
| height: 6px; | |
| background: #667eea; | |
| border-radius: 50%; | |
| animation: typing-bounce 1.4s infinite ease-in-out; | |
| }} | |
| .typing-dot:nth-child(1) {{ animation-delay: -0.32s; }} | |
| .typing-dot:nth-child(2) {{ animation-delay: -0.16s; }} | |
| .typing-dot:nth-child(3) {{ animation-delay: 0s; }} | |
| @keyframes typing-bounce {{ | |
| 0%, 80%, 100% {{ | |
| transform: scale(0.8); | |
| opacity: 0.5; | |
| }} | |
| 40% {{ | |
| transform: scale(1); | |
| opacity: 1; | |
| }} | |
| }} | |
| .cursor {{ | |
| animation: blink 1s infinite; | |
| color: #667eea; | |
| font-weight: bold; | |
| }} | |
| @keyframes blink {{ | |
| 0%, 50% {{ opacity: 1; }} | |
| 51%, 100% {{ opacity: 0; }} | |
| }} | |
| </style> | |
| ''' |