PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/search-chat.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 16:59:25 UTC 5,491 bytes, 201 lines download raw bytes

1// Search Chat Module — Ctrl+K command palette for searching conversations
2
3import uiModule from './ui.js';
4import sessionModule from './sessions.js';
5
6let API_BASE = '';
7let debounceTimer = null;
8let selectedIndex = -1;
9let results = [];
10
11function el(id) { return document.getElementById(id); }
12
13export function openSearch() {
14  const overlay = el('search-overlay');
15  if (!overlay) return;
16  overlay.classList.remove('hidden');
17  const input = el('search-input');
18  if (input) {
19    input.value = '';
20    input.focus();
21  }
22  selectedIndex = -1;
23  results = [];
24  el('search-results').innerHTML = '';
25}
26
27export function closeSearch() {
28  const overlay = el('search-overlay');
29  if (!overlay) return;
30  overlay.classList.add('hidden');
31  el('search-results').innerHTML = '';
32  selectedIndex = -1;
33  results = [];
34}
35
36export function isOpen() {
37  const overlay = el('search-overlay');
38  return overlay && !overlay.classList.contains('hidden');
39}
40
41var escapeHtml = uiModule.esc;
42
43function highlightMatch(text, query) {
44  if (!query) return escapeHtml(text);
45  const escaped = escapeHtml(text);
46  const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
47  return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
48}
49
50function formatTimestamp(iso) {
51  if (!iso) return '';
52  const d = new Date(iso);
53  const now = new Date();
54  const diff = now - d;
55  if (diff < 86400000) {
56    return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
57  }
58  if (diff < 604800000) {
59    return d.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' });
60  }
61  return d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
62}
63
64function renderResults(data, query) {
65  results = data;
66  selectedIndex = -1;
67  const container = el('search-results');
68  if (!container) return;
69
70  if (!data || data.length === 0) {
71    container.innerHTML = query
72      ? '<div class="search-empty">No results found</div>'
73      : '';
74    return;
75  }
76
77  // Group by session
78  const grouped = {};
79  for (const r of data) {
80    if (!grouped[r.session_id]) {
81      grouped[r.session_id] = { name: r.session_name, items: [] };
82    }
83    grouped[r.session_id].items.push(r);
84  }
85
86  let html = '';
87  let idx = 0;
88  for (const [sessionId, group] of Object.entries(grouped)) {
89    html += `<div class="search-group-header">${escapeHtml(group.name)}</div>`;
90    for (const item of group.items) {
91      const roleLabel = item.role === 'user' ? 'You' : 'AI';
92      html += `<div class="search-result-item" data-index="${idx}" data-session="${escapeHtml(sessionId)}">
93        <div class="search-result-role">${roleLabel}</div>
94        <div class="search-result-snippet">${highlightMatch(item.content_snippet, query)}</div>
95        <div class="search-result-time">${formatTimestamp(item.timestamp)}</div>
96      </div>`;
97      idx++;
98    }
99  }
100  container.innerHTML = html;
101
102  // Click handlers
103  container.querySelectorAll('.search-result-item').forEach(item => {
104    item.addEventListener('click', () => {
105      const sid = item.dataset.session;
106      navigateToSession(sid);
107    });
108  });
109}
110
111function navigateToSession(sessionId) {
112  closeSearch();
113  if (sessionModule && sessionModule.selectSession) {
114    sessionModule.selectSession(sessionId);
115  }
116}
117
118function updateSelection() {
119  const container = el('search-results');
120  if (!container) return;
121  const items = container.querySelectorAll('.search-result-item');
122  items.forEach((item, i) => {
123    item.classList.toggle('selected', i === selectedIndex);
124  });
125  // Scroll selected into view
126  if (selectedIndex >= 0 && items[selectedIndex]) {
127    items[selectedIndex].scrollIntoView({ block: 'nearest' });
128  }
129}
130
131function handleKeydown(e) {
132  if (!isOpen()) return;
133
134  const container = el('search-results');
135  const items = container ? container.querySelectorAll('.search-result-item') : [];
136  const count = items.length;
137
138  if (e.key === 'ArrowDown') {
139    e.preventDefault();
140    selectedIndex = count > 0 ? Math.min(selectedIndex + 1, count - 1) : -1;
141    updateSelection();
142  } else if (e.key === 'ArrowUp') {
143    e.preventDefault();
144    selectedIndex = Math.max(selectedIndex - 1, 0);
145    updateSelection();
146  } else if (e.key === 'Enter') {
147    e.preventDefault();
148    if (selectedIndex >= 0 && items[selectedIndex]) {
149      const sid = items[selectedIndex].dataset.session;
150      navigateToSession(sid);
151    }
152  }
153}
154
155function handleInput(e) {
156  const query = e.target.value.trim();
157  if (debounceTimer) clearTimeout(debounceTimer);
158
159  if (!query) {
160    renderResults([], '');
161    return;
162  }
163
164  debounceTimer = setTimeout(async () => {
165    try {
166      const res = await fetch(`${API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=20`);
167      if (!res.ok) return;
168      const data = await res.json();
169      renderResults(data, query);
170    } catch (err) {
171      console.error('Search error:', err);
172    }
173  }, 300);
174}
175
176export function init(apiBase) {
177  API_BASE = apiBase || '';
178
179  const input = el('search-input');
180  if (input) {
181    input.addEventListener('input', handleInput);
182    input.addEventListener('keydown', handleKeydown);
183  }
184
185  // Close on overlay click (not popup click)
186  const overlay = el('search-overlay');
187  if (overlay) {
188    overlay.addEventListener('click', (e) => {
189      if (e.target === overlay) closeSearch();
190    });
191  }
192}
193
194const searchChatModule = {
195  init,
196  openSearch,
197  closeSearch,
198  isOpen,
199};
200
201export default searchChatModule;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.