Prechádzať zdrojové kódy

TG-32 TG-33: Implement elegant responsive dropdown search UI securely mapped to AI backend

FerRo988 4 týždňov pred
rodič
commit
f884e1028f
3 zmenil súbory, kde vykonal 301 pridanie a 0 odobranie
  1. 14 0
      static/index.html
  2. 100 0
      static/script.js
  3. 187 0
      static/style.css

+ 14 - 0
static/index.html

@@ -27,6 +27,20 @@
             </div>
         </header>
         
+        <!-- New Visual Food Search Component -->
+        <div class="search-module" id="food-search-module">
+            <div class="search-input-wrapper">
+                <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
+                <input type="text" id="food-search-input" placeholder="Search for standard foods or raw ingredients..." autocomplete="off">
+                <button id="clear-search-btn" style="display: none;" title="Clear search">
+                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
+                </button>
+            </div>
+            <div class="search-results-dropdown" id="search-results-dropdown" style="display: none;">
+                <!-- Dynamically populated by JS fetch -->
+            </div>
+        </div>
+        
         <main class="chat-container" id="chat-container">
             <div class="message system">
                 <div class="avatar">🤖</div>

+ 100 - 0
static/script.js

@@ -358,6 +358,106 @@ document.addEventListener('DOMContentLoaded', () => {
         }
     });
 
+    // --- Food Search Module Logic ---
+    const searchInput = document.getElementById('food-search-input');
+    const searchDropdown = document.getElementById('search-results-dropdown');
+    const clearSearchBtn = document.getElementById('clear-search-btn');
+
+    function debounce(func, delay) {
+        let timeout;
+        return function(...args) {
+            clearTimeout(timeout);
+            timeout = setTimeout(() => func(...args), delay);
+        };
+    }
+
+    const performSearch = async (query) => {
+        if (!query) {
+            searchDropdown.style.display = 'none';
+            return;
+        }
+        
+        searchDropdown.style.display = 'block';
+        searchDropdown.innerHTML = '<div class="search-loading">Searching local database...</div>';
+        
+        try {
+            const token = localStorage.getItem('localFoodToken');
+            const response = await fetch(`/api/food/search?q=${encodeURIComponent(query)}`, {
+                headers: { 'Authorization': `Bearer ${token}` }
+            });
+            const data = await response.json();
+            
+            if (data.results && data.results.length > 0) {
+                searchDropdown.innerHTML = '';
+                data.results.forEach(item => {
+                    const foodEl = document.createElement('div');
+                    foodEl.className = 'food-item';
+                    
+                    const badgeStr = item.category === "Sourced Ingredient" ? "Raw Ingredient" : item.category;
+                    
+                    foodEl.innerHTML = `
+                        <div class="food-item-header">
+                            <span class="food-name">${item.name}</span>
+                            <span class="food-badge">${badgeStr}</span>
+                        </div>
+                        <div class="food-macros">
+                            <div class="macro-tag">Cal: <span>${item.calories}</span></div>
+                            <div class="macro-tag">Pro: <span>${item.protein_g}g</span></div>
+                            <div class="macro-tag">Fat: <span>${item.fat_g}g</span></div>
+                            <div class="macro-tag">Carb: <span>${item.carbs_g}g</span></div>
+                        </div>
+                    `;
+                    
+                    foodEl.addEventListener('click', () => {
+                        userInput.value = `Can you build a meal around ${item.name} (${item.calories} cal, ${item.protein_g}g protein)?`;
+                        userInput.focus();
+                        userInput.style.height = 'auto'; // Reset
+                        sendBtn.disabled = false;
+                        searchDropdown.style.display = 'none';
+                        searchInput.value = '';
+                        clearSearchBtn.style.display = 'none';
+                    });
+                    
+                    searchDropdown.appendChild(foodEl);
+                });
+            } else {
+                searchDropdown.innerHTML = '<div class="search-empty">No matching foods found.</div>';
+            }
+        } catch (error) {
+            searchDropdown.innerHTML = '<div class="search-empty">Error searching database.</div>';
+        }
+    };
+
+    const handleSearchInput = debounce((e) => {
+        const query = e.target.value.trim();
+        if (query.length > 0) {
+            clearSearchBtn.style.display = 'block';
+            performSearch(query);
+        } else {
+            clearSearchBtn.style.display = 'none';
+            searchDropdown.style.display = 'none';
+        }
+    }, 300);
+
+    if (searchInput) {
+        searchInput.addEventListener('input', handleSearchInput);
+    }
+    
+    if (clearSearchBtn) {
+        clearSearchBtn.addEventListener('click', () => {
+            searchInput.value = '';
+            clearSearchBtn.style.display = 'none';
+            searchDropdown.style.display = 'none';
+            searchInput.focus();
+        });
+    }
+
+    document.addEventListener('click', (e) => {
+        if (!e.target.closest('#food-search-module')) {
+            if (searchDropdown) searchDropdown.style.display = 'none';
+        }
+    });
+
     // Initialize state
     sendBtn.disabled = true;
 });

+ 187 - 0
static/style.css

@@ -480,3 +480,190 @@ textarea::placeholder {
     transform: scale(1);
 }
 
+/* --------------------------------- */
+/* Food Search Component Styles      */
+/* --------------------------------- */
+
+.search-module {
+    padding: 10px 24px;
+    background: rgba(22, 27, 34, 0.4);
+    border-bottom: 1px solid var(--border-color);
+    position: relative;
+    z-index: 10;
+}
+
+.search-input-wrapper {
+    position: relative;
+    display: flex;
+    align-items: center;
+    background: rgba(13, 17, 23, 0.8);
+    border: 1px solid var(--border-color);
+    border-radius: 12px;
+    padding: 8px 16px;
+    transition: all 0.3s ease;
+    box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.search-input-wrapper:focus-within {
+    border-color: rgba(46, 160, 67, 0.6);
+    box-shadow: 0 0 0 3px rgba(46, 160, 67, 0.15), inset 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.search-icon {
+    color: var(--text-muted);
+    margin-right: 12px;
+}
+
+#food-search-input {
+    flex: 1;
+    background: transparent;
+    border: none;
+    color: var(--text-main);
+    font-size: 0.95rem;
+    outline: none;
+    font-family: inherit;
+    width: 100%;
+}
+
+#clear-search-btn {
+    background: transparent;
+    border: none;
+    color: var(--text-muted);
+    cursor: pointer;
+    border-radius: 50%;
+    padding: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+}
+
+#clear-search-btn:hover {
+    background: rgba(255, 255, 255, 0.1);
+    color: #f85149;
+}
+
+.search-results-dropdown {
+    position: absolute;
+    top: 100%;
+    left: 24px;
+    right: 24px;
+    margin-top: 8px;
+    background: rgba(22, 27, 34, 0.85);
+    backdrop-filter: blur(20px);
+    -webkit-backdrop-filter: blur(20px);
+    border: 1px solid var(--border-color);
+    border-radius: 12px;
+    box-shadow: 0 15px 35px rgba(0,0,0,0.4);
+    max-height: 350px;
+    overflow-y: auto;
+    z-index: 100;
+    opacity: 0;
+    transform: translateY(-10px);
+    animation: dropdownFadeIn 0.2s forwards;
+}
+
+@keyframes dropdownFadeIn {
+    to { opacity: 1; transform: translateY(0); }
+}
+
+.search-results-dropdown::-webkit-scrollbar {
+    width: 6px;
+}
+.search-results-dropdown::-webkit-scrollbar-thumb {
+    background: rgba(255,255,255,0.2);
+    border-radius: 10px;
+}
+
+.food-item {
+    padding: 14px 18px;
+    border-bottom: 1px solid rgba(255,255,255,0.05);
+    cursor: pointer;
+    transition: background 0.2s, padding-left 0.2s;
+}
+
+.food-item:last-child {
+    border-bottom: none;
+}
+
+.food-item:hover {
+    background: rgba(255,255,255,0.05);
+    padding-left: 22px;
+}
+
+.food-item-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+}
+
+.food-name {
+    font-weight: 600;
+    color: #e6edf3;
+    font-size: 1rem;
+}
+
+.food-badge {
+    background: rgba(46, 160, 67, 0.15);
+    color: #3fb950;
+    border: 1px solid rgba(46, 160, 67, 0.3);
+    padding: 2px 8px;
+    border-radius: 12px;
+    font-size: 0.7rem;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+}
+
+.food-macros {
+    display: flex;
+    gap: 15px;
+    font-size: 0.8rem;
+    color: var(--text-muted);
+}
+
+.macro-tag span {
+    color: #c9d1d9;
+    font-weight: 500;
+}
+
+.search-loading, .search-empty {
+    padding: 20px;
+    text-align: center;
+    color: var(--text-muted);
+    font-size: 0.9rem;
+    background: transparent;
+}
+
+/* Mobile Responsiveness */
+@media (max-width: 600px) {
+    body {
+        padding: 0;
+    }
+    .app-container {
+        height: 100vh;
+        border-radius: 0;
+        border: none;
+    }
+    .chat-header {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 10px;
+    }
+    .actions {
+        width: 100%;
+        justify-content: space-between;
+    }
+    .search-module {
+        padding: 10px 12px;
+    }
+    .search-results-dropdown {
+        left: 12px;
+        right: 12px;
+    }
+    .message {
+        max-width: 95%;
+    }
+}
+