personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 933 lines 24 kB view raw
1<style> 2/* Layout */ 3.search-container { 4 display: flex; 5 gap: 1.5rem; 6 min-height: calc(100vh - 200px); 7} 8 9/* Sidebar */ 10.search-sidebar { 11 width: 200px; 12 flex-shrink: 0; 13 border-right: 1px solid #e5e7eb; 14 padding-left: 0.5rem; 15 padding-right: 0.75rem; 16 padding-top: 0.45rem; 17} 18 19.search-sidebar h3 { 20 font-size: 11px; 21 font-weight: 600; 22 color: #9ca3af; 23 text-transform: uppercase; 24 letter-spacing: 0.05em; 25 margin: 0 0 0.5rem 0; 26} 27 28.filter-section { 29 margin-bottom: 1.25rem; 30} 31 32.filter-section:not(:last-child) { 33 border-bottom: 1px solid #e5e7eb; 34 padding-bottom: 1.25rem; 35} 36 37.filter-list { 38 display: flex; 39 flex-direction: column; 40 gap: 0.25rem; 41} 42 43.filter-item { 44 display: flex; 45 align-items: center; 46 gap: 0.5rem; 47 padding: 0.35rem 0.5rem; 48 border-radius: 4px; 49 cursor: pointer; 50 font-size: 0.85rem; 51 transition: background 0.15s; 52} 53 54.filter-item:hover { 55 background: #f3f4f6; 56} 57 58.filter-item.active { 59 background: #e5e7eb; 60 box-shadow: inset 3px 0 0 var(--facet-color, #b06a1a); 61 font-weight: 500; 62} 63 64.filter-item:active { 65 background: #e5e7eb; 66} 67 68.filter-item input[type="radio"] { 69 margin: 0; 70 accent-color: var(--facet-color, #2563eb); 71} 72 73.filter-item input[type="radio"]:focus-visible { 74 outline: 2px solid var(--facet-color, #b06a1a); 75 outline-offset: 2px; 76} 77 78.filter-label { 79 flex: 1; 80 min-width: 0; 81 overflow: hidden; 82 text-overflow: ellipsis; 83 white-space: nowrap; 84} 85 86.filter-count { 87 font-size: 0.75rem; 88 color: #9ca3af; 89 flex-shrink: 0; 90} 91 92.filter-icon { 93 font-size: 0.85rem; 94} 95 96/* Main content area */ 97.search-main { 98 flex: 1; 99 min-width: 0; 100 display: flex; 101 flex-direction: column; 102} 103 104#results-container { 105 flex: 1; 106 display: flex; 107 flex-direction: column; 108} 109 110/* Results summary */ 111.search-summary { 112 font-size: 0.85rem; 113 color: #6b7280; 114 margin-bottom: 1rem; 115 font-variant-numeric: tabular-nums; 116} 117 118/* Day cards */ 119.day-card { 120 background: white; 121 border-radius: 8px; 122 margin-bottom: 0.75rem; 123 box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); 124 overflow: hidden; 125} 126 127.day-header { 128 display: flex; 129 align-items: center; 130 justify-content: space-between; 131 padding: 0.85rem 1rem; 132 background: #f9fafb; 133 border-bottom: 1px solid #e5e7eb; 134 cursor: pointer; 135 transition: background 0.1s ease; 136} 137 138.day-header:hover { 139 background: #f3f4f6; 140} 141 142.day-header:active { 143 background: #e5e7eb; 144} 145 146button.day-header { 147 border: none; 148 font: inherit; 149 text-align: left; 150 width: 100%; 151} 152 153.day-date { 154 font-size: 0.9rem; 155 font-weight: 600; 156 color: #111827; 157} 158 159.day-count { 160 font-size: 0.8rem; 161 color: #6b7280; 162} 163 164.day-results { 165 padding: 0; 166} 167 168/* Result items */ 169.result-item { 170 padding: 0.75rem 1rem; 171 border-bottom: 1px solid #e5e7eb; 172 display: block; 173 text-decoration: none; 174 color: inherit; 175 transition: background 0.1s ease; 176} 177 178.result-item:last-child { 179 border-bottom: none; 180} 181 182.result-item:hover { 183 background: #f9fafb; 184} 185 186.result-item:active { 187 background: #f3f4f6; 188} 189 190.result-meta { 191 display: flex; 192 align-items: center; 193 gap: 0.5rem; 194 margin-bottom: 0.35rem; 195 flex-wrap: wrap; 196} 197 198.result-agent { 199 display: flex; 200 align-items: center; 201 gap: 0.25rem; 202 font-size: 0.8rem; 203 font-weight: 400; 204 color: #374151; 205} 206 207.result-agent-icon { 208 font-size: 0.9rem; 209} 210 211.result-facet { 212 font-size: 0.7rem; 213 padding: 0.15rem 0.4rem; 214 border-radius: 3px; 215 background: #f3f4f6; 216 color: #6b7280; 217 letter-spacing: 0.02em; 218} 219 220.result-facet.has-color { 221 color: white; 222} 223 224.result-text { 225 font-size: 0.9rem; 226 color: #4b5563; 227 line-height: 1.5; 228 /* Fill available space */ 229 display: -webkit-box; 230 -webkit-line-clamp: 3; 231 -webkit-box-orient: vertical; 232 overflow: hidden; 233} 234 235.result-text strong { 236 color: #111827; 237 background: #fef3c7; 238 padding: 1px 3px; 239 border-radius: 3px; 240 font-weight: 600; 241} 242 243/* Show more button */ 244.show-more-btn { 245 display: block; 246 width: 100%; 247 padding: 0.6rem; 248 background: #f9fafb; 249 border: none; 250 border-top: 1px solid #e5e7eb; 251 color: var(--facet-color, #2563eb); 252 font-size: 0.85rem; 253 cursor: pointer; 254 transition: background 0.1s ease; 255 text-align: center; 256} 257 258.show-more-btn:hover { 259 background: #f3f4f6; 260} 261 262.show-more-btn:active { 263 background: #e5e7eb; 264} 265 266.show-more-btn:disabled { 267 color: #9ca3af; 268 cursor: not-allowed; 269} 270 271/* Load more days */ 272.load-more-days { 273 display: block; 274 width: 100%; 275 padding: 0.75rem; 276 background: white; 277 border: 1px solid #e5e7eb; 278 border-radius: 8px; 279 color: var(--facet-color, #2563eb); 280 font-size: 0.85rem; 281 cursor: pointer; 282 transition: background 0.1s ease; 283 text-align: center; 284 margin-top: 0; 285} 286 287.load-more-days:hover { 288 background: #f9fafb; 289} 290 291.load-more-days:active { 292 background: #f3f4f6; 293} 294 295/* Search input */ 296.search-input-form { 297 margin-bottom: 1rem; 298 margin-left: auto; 299 margin-right: auto; 300 max-width: 640px; 301} 302 303.search-input { 304 width: 100%; 305 padding: 0.75rem 1rem; 306 font-size: 1rem; 307 border: 1px solid #c8c0b8; 308 border-radius: 8px; 309 outline: none; 310 transition: border-color 0.15s, box-shadow 0.15s; 311 box-sizing: border-box; 312} 313 314.search-input:focus { 315 border-color: var(--facet-color, #2563eb); 316 box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 317} 318 319.search-input:hover:not(:focus) { 320 border-color: #b8b0a8; 321} 322 323.search-input::placeholder { 324 color: #aaa; 325} 326 327/* No-results suggestions */ 328.search-noresults-suggestions { 329 margin-top: 1rem; 330 font-size: 0.85rem; 331 color: #6b7280; 332 display: flex; 333 flex-direction: column; 334 gap: 0.4rem; 335 align-items: center; 336} 337 338.search-noresults-suggestions button { 339 background: none; 340 border: 1px solid #d1d5db; 341 border-radius: 0.375rem; 342 padding: 0.25rem 0.75rem; 343 min-height: 44px; 344 display: inline-flex; 345 align-items: center; 346 color: #4b5563; 347 font-size: 0.85rem; 348 cursor: pointer; 349 transition: background 0.1s ease, border-color 0.1s ease; 350} 351 352.search-noresults-suggestions button:hover { 353 background: #f3f4f6; 354 border-color: #9ca3af; 355} 356 357.search-noresults-suggestions button:active { 358 background: #e5e7eb; 359 border-color: #6b7280; 360} 361 362.search-agents-empty { 363 color: #9ca3af; 364 font-size: 0.85rem; 365 padding: 0.25rem 0.5rem; 366} 367 368.search-example-chips { 369 display: flex; 370 flex-wrap: wrap; 371 gap: 0.5rem; 372 justify-content: center; 373} 374 375.search-example-chip { 376 display: inline-flex; 377 align-items: center; 378 padding: 0.4em 0.85em; 379 background: #f3f4f6; 380 border: 1px solid #ebe6e1; 381 border-radius: 20px; 382 font-size: 0.85rem; 383 color: #374151; 384 cursor: pointer; 385 transition: all 0.15s; 386 font-family: inherit; 387} 388 389.search-example-chip:hover { 390 background: #e5e7eb; 391 border-color: #ddd8d3; 392} 393 394.search-example-chip:focus-visible { 395 outline: 2px solid var(--facet-color, #b06a1a); 396 outline-offset: 2px; 397} 398 399.search-example-chip:active { 400 background: #d1d5db; 401} 402 403/* Loading state */ 404/* Responsive */ 405@media (max-width: 768px) { 406 .search-container { 407 flex-direction: column; 408 } 409 410 .search-sidebar { 411 width: 100%; 412 display: flex; 413 gap: 1rem; 414 flex-wrap: wrap; 415 border-right: none; 416 margin-bottom: 0.25rem; 417 padding-right: 0; 418 } 419 420 .search-input-form { 421 margin-bottom: 0.75rem; 422 margin-left: auto; 423 margin-right: auto; 424 } 425 426 .filter-section { 427 flex: 1; 428 min-width: 150px; 429 margin-bottom: 0.5rem; 430 } 431 432 .filter-section:not(:last-child) { 433 border-bottom: none; 434 padding-bottom: 0; 435 } 436 437 .filter-list { 438 flex-direction: row; 439 flex-wrap: wrap; 440 } 441 442 .filter-item { 443 flex: 0 0 auto; 444 } 445} 446</style> 447 448<div class="workspace-content-wide"> 449 <div class="search-container"> 450 <!-- Sidebar --> 451 <aside class="search-sidebar"> 452 <div class="filter-section" id="facet-filters"> 453 <h3>facets</h3> 454 <div class="filter-list" id="facet-list"> 455 <label class="filter-item active"> 456 <input type="radio" name="facet" value="" checked> 457 <span class="filter-label">all</span> 458 </label> 459 </div> 460 </div> 461 462 <div class="filter-section" id="agent-filters"> 463 <h3>agents</h3> 464 <div class="filter-list" id="agent-list"> 465 <span class="search-agents-empty">agents appear here as your journal grows</span> 466 </div> 467 </div> 468 </aside> 469 470 <!-- Main content --> 471 <main class="search-main"> 472 <form class="search-input-form" id="search-input-form" role="search"> 473 <label for="search-input" class="visually-hidden">search your journal</label> 474 <input type="search" class="search-input" id="search-input" 475 placeholder="search your journal..." autocomplete="off"> 476 </form> 477 478 <div class="search-summary" id="search-summary" role="status" aria-live="polite" style="display: none;"></div> 479 <div id="search-announcer" class="visually-hidden" aria-live="polite"></div> 480 481 <div id="results-container"> 482 <div class="surface-state surface-state--empty" id="search-empty"> 483 <div class="surface-state-icon" aria-hidden="true">🔍</div> 484 <h2 class="surface-state-heading">search your journal</h2> 485 <p class="surface-state-desc">find conversations, ideas, and moments — try one of these</p> 486 <div class="surface-state-action"> 487 <div class="search-example-chips"> 488 <button class="search-example-chip" type="button">a recent meeting</button> 489 <button class="search-example-chip" type="button">something i learned</button> 490 <button class="search-example-chip" type="button">project ideas</button> 491 </div> 492 </div> 493 </div> 494 </div> 495 496 <button id="load-more-days" class="load-more-days" style="display: none;"> 497 Load more days 498 </button> 499 </main> 500 </div> 501</div> 502 503<script> 504(function() { 505 const searchUrl = '{{ url_for("app:search.search_journal_api") }}'; 506 const dayResultsUrl = '{{ url_for("app:search.day_results_api") }}'; 507 const activitiesDayBase = '{{ url_for("app:activities.activities_day", day="") }}'; 508 const escapeHtml = window.AppServices.escapeHtml; 509 510 // State 511 let currentQuery = ''; 512 let currentFacet = ''; 513 let currentAgent = ''; 514 let dayOffset = 0; 515 let searchController = null; 516 let loadMoreController = null; 517 518 // DOM elements 519 const resultsContainer = document.getElementById('results-container'); 520 const searchSummary = document.getElementById('search-summary'); 521 const searchEmpty = document.getElementById('search-empty'); 522 const loadMoreDaysBtn = document.getElementById('load-more-days'); 523 const facetList = document.getElementById('facet-list'); 524 const agentList = document.getElementById('agent-list'); 525 const searchInput = document.getElementById('search-input'); 526 const searchInputForm = document.getElementById('search-input-form'); 527 const searchAnnouncer = document.getElementById('search-announcer'); 528 529 function bootstrapSearch() { 530 loadFromHash(); 531 searchInput.focus(); 532 } 533 534 if (document.readyState === 'loading') { 535 document.addEventListener('DOMContentLoaded', bootstrapSearch, { once: true }); 536 } else { 537 bootstrapSearch(); 538 } 539 540 // Example chip delegation 541 resultsContainer.addEventListener('click', function(e) { 542 const chip = e.target.closest('.search-example-chip'); 543 if (!chip) return; 544 searchInput.value = chip.textContent; 545 currentQuery = chip.textContent; 546 dayOffset = 0; 547 updateHash(); 548 doSearch(true); 549 }); 550 551 window.addEventListener('hashchange', loadFromHash); 552 553 searchInputForm.onsubmit = function(e) { 554 e.preventDefault(); 555 const query = searchInput.value.trim(); 556 currentQuery = query; 557 dayOffset = 0; 558 updateHash(); 559 if (query) { 560 doSearch(true); 561 } else { 562 showEmpty(); 563 } 564 }; 565 566 // Listen for search submissions from app bar 567 window.addEventListener('search.submit', (e) => { 568 const query = e.detail.query; 569 if (!query) return; 570 currentQuery = query; 571 searchInput.value = query; 572 dayOffset = 0; 573 updateHash(); 574 doSearch(true); 575 }); 576 577 function loadFromHash() { 578 const hashParams = new URLSearchParams(window.location.hash.slice(1)); 579 const q = hashParams.get('q') || ''; 580 const f = hashParams.get('facet') || ''; 581 const t = hashParams.get('agent') || ''; 582 583 if (q !== currentQuery || f !== currentFacet || t !== currentAgent) { 584 currentQuery = q; 585 currentFacet = f; 586 currentAgent = t; 587 588 // Notify app bar of query change 589 window.dispatchEvent(new CustomEvent('search.queryUpdate', { 590 detail: { query: q } 591 })); 592 searchInput.value = q; 593 594 if (q) { 595 doSearch(true); 596 } else { 597 showEmpty(); 598 } 599 } 600 } 601 602 function updateHash() { 603 const params = new URLSearchParams(); 604 if (currentQuery) params.set('q', currentQuery); 605 if (currentFacet) params.set('facet', currentFacet); 606 if (currentAgent) params.set('agent', currentAgent); 607 window.location.hash = params.toString(); 608 } 609 610 // Load more days 611 loadMoreDaysBtn.onclick = function() { 612 dayOffset += 20; 613 doSearch(false); 614 }; 615 616 function announce(msg) { 617 searchAnnouncer.textContent = ''; 618 setTimeout(() => { searchAnnouncer.textContent = msg; }, 100); 619 } 620 621 // Main search function 622 function doSearch(reset) { 623 if (!currentQuery) { 624 showEmpty(); 625 return; 626 } 627 628 searchController?.abort(); 629 searchController = new AbortController(); 630 631 if (reset) { 632 dayOffset = 0; 633 resultsContainer.innerHTML = window.SurfaceState.loading({ text: 'Searching...' }); 634 announce('searching…'); 635 } 636 637 const url = new URL(searchUrl, window.location.origin); 638 url.searchParams.set('q', currentQuery); 639 url.searchParams.set('offset', dayOffset); 640 if (currentFacet) url.searchParams.set('facet', currentFacet); 641 if (currentAgent) url.searchParams.set('agent', currentAgent); 642 643 fetch(url, { signal: searchController.signal }) 644 .then(r => { 645 if (!r.ok) throw new Error('search returned ' + r.status); 646 return r.json(); 647 }) 648 .then(data => { 649 if (reset) { 650 resultsContainer.innerHTML = ''; 651 renderFilters(data.facets, data.agents); 652 } 653 654 renderResults(data, reset); 655 updateSummary(data); 656 updateLoadMoreButton(data); 657 }) 658 .catch(err => { 659 if (err.name === 'AbortError') return; 660 console.error('Search error:', err); 661 resultsContainer.innerHTML = window.SurfaceState.error({ 662 icon: '⚠️', 663 heading: 'something went wrong with that search', 664 desc: 'could be a network issue or the server might be busy — give it a moment', 665 action: '<button class="search-error-retry">try again</button>' 666 }); 667 resultsContainer.querySelector('.search-error-retry').onclick = () => doSearch(true); 668 announce('search failed, use the try again button to retry'); 669 }); 670 } 671 672 function showEmpty() { 673 resultsContainer.innerHTML = ''; 674 resultsContainer.appendChild(searchEmpty.cloneNode(true)); 675 searchSummary.style.display = 'none'; 676 loadMoreDaysBtn.style.display = 'none'; 677 } 678 679 function renderFilters(facets, agents) { 680 // Render facet filters 681 let facetHtml = ` 682 <label class="filter-item ${!currentFacet ? 'active' : ''}"> 683 <input type="radio" name="facet" value="" ${!currentFacet ? 'checked' : ''}> 684 <span class="filter-label">all</span> 685 </label> 686 `; 687 688 for (const f of facets) { 689 const isActive = currentFacet === f.name; 690 const emoji = f.emoji ? `<span class="filter-icon">${f.emoji}</span>` : ''; 691 facetHtml += ` 692 <label class="filter-item ${isActive ? 'active' : ''}"> 693 <input type="radio" name="facet" value="${escapeHtml(f.name)}" ${isActive ? 'checked' : ''}> 694 ${emoji} 695 <span class="filter-label">${escapeHtml(f.title || f.name)}</span> 696 <span class="filter-count">${f.count}</span> 697 </label> 698 `; 699 } 700 facetList.innerHTML = facetHtml; 701 702 // Add facet change handlers 703 facetList.querySelectorAll('input[type="radio"]').forEach(input => { 704 input.onchange = function() { 705 currentFacet = this.value; 706 facetList.querySelectorAll('.filter-item').forEach(item => { 707 item.classList.toggle('active', item.querySelector('input').value === currentFacet); 708 }); 709 updateHash(); 710 doSearch(true); 711 }; 712 }); 713 714 // Render agent filters (radio buttons for single-select) 715 if (agents.length === 0) { 716 agentList.innerHTML = '<span class="search-agents-empty">agents appear here as your journal grows</span>'; 717 } else { 718 let agentHtml = ` 719 <label class="filter-item ${!currentAgent ? 'active' : ''}"> 720 <input type="radio" name="agent" value="" ${!currentAgent ? 'checked' : ''}> 721 <span class="filter-label">all</span> 722 </label> 723 `; 724 for (const t of agents) { 725 const isActive = currentAgent === t.name; 726 agentHtml += ` 727 <label class="filter-item ${isActive ? 'active' : ''}"> 728 <input type="radio" name="agent" value="${escapeHtml(t.name)}" ${isActive ? 'checked' : ''}> 729 <span class="filter-icon">${t.icon}</span> 730 <span class="filter-label">${escapeHtml(t.label)}</span> 731 <span class="filter-count">${t.count}</span> 732 </label> 733 `; 734 } 735 agentList.innerHTML = agentHtml; 736 737 // Add agent change handlers 738 agentList.querySelectorAll('input[type="radio"]').forEach(input => { 739 input.onchange = function() { 740 currentAgent = this.value; 741 agentList.querySelectorAll('.filter-item').forEach(item => { 742 item.classList.toggle('active', item.querySelector('input').value === currentAgent); 743 }); 744 updateHash(); 745 doSearch(true); 746 }; 747 }); 748 } 749 } 750 751 function renderResults(data, reset) { 752 if (data.days.length === 0 && reset) { 753 const suggestionsHtml = '<div class="search-noresults-suggestions">' 754 + '<span>try different keywords or check your spelling</span>' 755 + (currentFacet ? `<button type="button" data-clear="facet">search all facets instead of ${escapeHtml(currentFacet)}</button>` : '') 756 + (currentAgent ? `<button type="button" data-clear="agent">search all agents instead of ${escapeHtml(currentAgent)}</button>` : '') 757 + '</div>'; 758 759 resultsContainer.innerHTML = window.SurfaceState.empty({ 760 icon: '🔍', 761 heading: `no results found for "${currentQuery}"`, 762 action: suggestionsHtml 763 }); 764 765 const clearFacetBtn = resultsContainer.querySelector('[data-clear="facet"]'); 766 if (clearFacetBtn) { 767 clearFacetBtn.onclick = () => { 768 currentFacet = ''; 769 updateHash(); 770 doSearch(true); 771 }; 772 } 773 774 const clearAgentBtn = resultsContainer.querySelector('[data-clear="agent"]'); 775 if (clearAgentBtn) { 776 clearAgentBtn.onclick = () => { 777 currentAgent = ''; 778 updateHash(); 779 doSearch(true); 780 }; 781 } 782 783 let msg = 'no results found'; 784 if (currentFacet || currentAgent) msg += ', try clearing filters'; 785 announce(msg); 786 return; 787 } 788 789 for (const dayData of data.days) { 790 const card = createDayCard(dayData); 791 resultsContainer.appendChild(card); 792 } 793 } 794 795 function createDayCard(dayData) { 796 const card = document.createElement('div'); 797 card.className = 'day-card'; 798 card.dataset.day = dayData.day; 799 800 const header = document.createElement('button'); 801 header.className = 'day-header'; 802 header.innerHTML = ` 803 <span class="day-date">${escapeHtml(dayData.date)}</span> 804 <span class="day-count">${dayData.total} match${dayData.total !== 1 ? 'es' : ''}</span> 805 `; 806 header.onclick = function() { 807 window.location.href = activitiesDayBase + dayData.day; 808 }; 809 810 const resultsDiv = document.createElement('div'); 811 resultsDiv.className = 'day-results'; 812 813 for (const result of dayData.results) { 814 resultsDiv.appendChild(createResultItem(result)); 815 } 816 817 card.appendChild(header); 818 card.appendChild(resultsDiv); 819 820 // Show more button if needed 821 if (dayData.has_more) { 822 const showMoreBtn = document.createElement('button'); 823 showMoreBtn.className = 'show-more-btn'; 824 showMoreBtn.textContent = `Show ${dayData.total - dayData.showing} more`; 825 showMoreBtn.onclick = function(e) { 826 e.stopPropagation(); 827 loadMoreForDay(dayData.day, dayData.showing, resultsDiv, showMoreBtn); 828 }; 829 card.appendChild(showMoreBtn); 830 } 831 832 return card; 833 } 834 835 function createResultItem(result) { 836 const item = document.createElement('a'); 837 item.className = 'result-item'; 838 item.href = activitiesDayBase + result.day; 839 840 // Build facet badge 841 let facetBadge = ''; 842 if (result.facet) { 843 const style = result.facet_color 844 ? `background: ${result.facet_color}; color: white;` 845 : ''; 846 const hasColor = result.facet_color ? 'has-color' : ''; 847 const emoji = result.facet_emoji ? result.facet_emoji + ' ' : ''; 848 facetBadge = `<span class="result-facet ${hasColor}" style="${style}">${emoji}#${escapeHtml(result.facet)}</span>`; 849 } 850 851 item.innerHTML = ` 852 <div class="result-meta"> 853 <span class="result-agent"> 854 <span class="result-agent-icon">${result.agent_icon}</span> 855 ${escapeHtml(result.agent_label)} 856 </span> 857 ${facetBadge} 858 </div> 859 <div class="result-text">${result.text}</div> 860 `; 861 862 return item; 863 } 864 865 function loadMoreForDay(day, currentOffset, resultsDiv, btn) { 866 loadMoreController?.abort(); 867 loadMoreController = new AbortController(); 868 btn.textContent = 'loading...'; 869 btn.disabled = true; 870 871 const url = new URL(dayResultsUrl, window.location.origin); 872 url.searchParams.set('q', currentQuery); 873 url.searchParams.set('day', day); 874 url.searchParams.set('offset', currentOffset); 875 url.searchParams.set('limit', 20); 876 if (currentFacet) url.searchParams.set('facet', currentFacet); 877 if (currentAgent) url.searchParams.set('agent', currentAgent); 878 879 fetch(url, { signal: loadMoreController.signal }) 880 .then(r => r.json()) 881 .then(data => { 882 for (const result of data.results) { 883 resultsDiv.appendChild(createResultItem(result)); 884 } 885 886 const newOffset = currentOffset + data.results.length; 887 const remaining = data.total - newOffset; 888 889 if (remaining > 0) { 890 btn.textContent = `Show ${remaining} more`; 891 btn.disabled = false; 892 btn.onclick = function(e) { 893 e.stopPropagation(); 894 loadMoreForDay(day, newOffset, resultsDiv, btn); 895 }; 896 } else { 897 btn.remove(); 898 } 899 }) 900 .catch(err => { 901 if (err.name === 'AbortError') return; 902 console.error('Load more error:', err); 903 btn.textContent = 'error - click to retry'; 904 btn.disabled = false; 905 }); 906 } 907 908 function updateSummary(data) { 909 if (data.total === 0) { 910 searchSummary.style.display = 'none'; 911 announce('no results found'); 912 return; 913 } 914 915 searchSummary.textContent = `${data.total} results across ${data.total_days} day${data.total_days !== 1 ? 's' : ''}`; 916 searchSummary.style.display = 'block'; 917 announce(searchSummary.textContent); 918 } 919 920 function updateLoadMoreButton(data) { 921 const totalDays = data.total_days; 922 const shownDays = dayOffset + data.showing_days; 923 924 if (shownDays < totalDays) { 925 loadMoreDaysBtn.textContent = `Load more days (${shownDays}/${totalDays})`; 926 loadMoreDaysBtn.style.display = 'block'; 927 } else { 928 loadMoreDaysBtn.style.display = 'none'; 929 } 930 } 931 932})(); 933</script>