PageSourceSearch

https://ewith.me/services/clientSearch.js?v=20261001_v62

js ewith.me collected 2026-10-02 02:00:47 UTC 9,508 bytes, 270 lines download raw bytes

1/**
2 * services/clientSearch.js
3 * ---------------------------------------------------------------------------
4 * 100% Client-Side Search Grounding for eWith.me AI Chat
5 * 
6 * Guarantees:
7 * 1. Zero Server Contact: Queries go directly from the user's browser to
8 *    DuckDuckGo (via JSONP) or Wikipedia (via CORS-open API). The eWith.me
9 *    server never sees, logs, or proxies the search queries.
10 * 2. Zero Latency for Unrelated Queries: A high-performance regex classifier
11 *    evaluates input in < 0.1ms. Coding, math, rewriting, and timeless
12 *    questions bypass search immediately with 0ms added delay.
13 * 3. Session Caching: In-memory Map prevents redundant network requests.
14 * ---------------------------------------------------------------------------
15 */
16
17// In-memory cache for the current browser session (30-minute TTL)
18const searchCache = new Map();
19const CACHE_TTL_MS = 30 * 60 * 1000;
20
21/**
22 * High-speed heuristic intent classifier (< 0.1ms).
23 * Determines if a prompt needs real-time / temporal grounding.
24 * 
25 * @param {string} query
26 * @returns {boolean} true if web search should be executed; false to bypass.
27 */
28export function shouldSearchWeb(query) {
29  if (!query || typeof query !== 'string') return false;
30  const trimmed = query.trim();
31  if (trimmed.length < 5) return false;
32
33  // 1. Instant Bypass Rules (Zero-latency fast paths)
34  // Code snippets, programming requests, and syntax markers
35  const hasCodeMarkers = /```|[\{\}\[\];=>]|\b(const|let|var|function|def|class|import|from|return|console\.log|SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|curl|npm|pip|docker)\b/.test(trimmed);
36  if (hasCodeMarkers) return false;
37
38  // Text transformation / editing operations
39  const isTransformation = /^(paraphrase|rewrite|summarize|translate|proofread|correct|fix grammar|rephrase|convert to|make this|format this|give me bullet points)\b/i.test(trimmed);
40  if (isTransformation) return false;
41
42  // Math, arithmetic, or formulas
43  const isMath = /^[\d\s\+\-\*\/\^\(\)\.\=\%\$]+$/.test(trimmed) || /\b(calculate|derivative|integral|matrix|solve for)\b/i.test(trimmed);
44  if (isMath) return false;
45
46  // 2. Temporal & Live Search Trigger Patterns
47  // Explicit mentions of current/future years (2024 to 2039)
48  if (/\b(202[4-9]|203\d)\b/.test(trimmed)) return true;
49
50  // Temporal adverbs & keywords
51  if (/\b(current|currently|latest|recent|recently|today|now|newest|upcoming|live|breaking|present)\b/i.test(trimmed)) return true;
52
53  // Question patterns about people, roles, or dynamic state
54  if (/\b(who is (the |currently )?(president|prime minister|ceo|leader|head|founder|champion|winner|director|dean|chief))\b/i.test(trimmed)) return true;
55
56  // Breaking news or status inquiries
57  if (/\b(what happened (to|in|at)|news (about|on|regarding|today)|price of|stock price|status of|release date|when (is|was|will))\b/i.test(trimmed)) return true;
58
59  // College / educational live information
60  if (/\b(ranking|admission dates|application deadline|cutoff score)\b/i.test(trimmed)) return true;
61
62  // Default: Timeless questions (concepts, explanations, philosophy, history) bypass search
63  return false;
64}
65
66/**
67 * Direct Client-Side DuckDuckGo Query via JSONP.
68 * Uses <script> tag injection to bypass browser CORS restrictions.
69 * Zero server touch: browser communicates directly with api.duckduckgo.com.
70 * 
71 * @param {string} query 
72 * @param {number} timeoutMs 
73 * @returns {Promise<Object|null>}
74 */
75export function searchDuckDuckGoClient(query, timeoutMs = 1500) {
76  return new Promise((resolve) => {
77    if (typeof window === 'undefined' || typeof document === 'undefined') {
78      return resolve(null);
79    }
80
81    const cbName = `ddg_cb_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
82    let scriptEl = null;
83    let timer = null;
84
85    const cleanup = () => {
86      if (timer) clearTimeout(timer);
87      if (scriptEl && scriptEl.parentNode) {
88        scriptEl.parentNode.removeChild(scriptEl);
89      }
90      try {
91        delete window[cbName];
92      } catch (_) {
93        window[cbName] = undefined;
94      }
95    };
96
97    timer = setTimeout(() => {
98      cleanup();
99      resolve(null);
100    }, timeoutMs);
101
102    window[cbName] = (data) => {
103      cleanup();
104      if (!data) return resolve(null);
105
106      const heading = data.Heading || '';
107      const abstract = data.AbstractText || data.Abstract || '';
108      const answer = data.Answer || '';
109      const definition = data.Definition || '';
110      const url = data.AbstractURL || '';
111
112      // Collect related topic snippets (up to 3)
113      const topics = [];
114      if (Array.isArray(data.RelatedTopics)) {
115        for (const item of data.RelatedTopics) {
116          if (topics.length >= 3) break;
117          if (item.Text && item.FirstURL) {
118            topics.push({ text: item.Text, url: item.FirstURL });
119          } else if (Array.isArray(item.Topics)) {
120            for (const sub of item.Topics) {
121              if (topics.length >= 3) break;
122              if (sub.Text && sub.FirstURL) {
123                topics.push({ text: sub.Text, url: sub.FirstURL });
124              }
125            }
126          }
127        }
128      }
129
130      if (!abstract && !answer && !definition && topics.length === 0) {
131        return resolve(null);
132      }
133
134      resolve({
135        heading,
136        abstract,
137        answer,
138        definition,
139        url,
140        topics
141      });
142    };
143
144    scriptEl = document.createElement('script');
145    scriptEl.src = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1&callback=${cbName}`;
146    scriptEl.async = true;
147    scriptEl.onerror = () => {
148      cleanup();
149      resolve(null);
150    };
151
152    document.head.appendChild(scriptEl);
153  });
154}
155
156/**
157 * Direct Client-Side Wikipedia Search via CORS-open API.
158 * Uses native fetch with origin=* parameter.
159 * 
160 * @param {string} query 
161 * @param {number} timeoutMs 
162 * @returns {Promise<Array<{title: string, snippet: string, url: string}>|null>}
163 */
164export async function searchWikipediaClient(query, timeoutMs = 1200) {
165  if (typeof fetch === 'undefined') return null;
166
167  const controller = new AbortController();
168  const timer = setTimeout(() => controller.abort(), timeoutMs);
169
170  try {
171    const url = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&origin=*`;
172    const res = await fetch(url, { signal: controller.signal });
173    clearTimeout(timer);
174
175    if (!res.ok) return null;
176    const data = await res.json();
177    const results = data?.query?.search || [];
178    if (results.length === 0) return null;
179
180    return results.slice(0, 2).map(r => ({
181      title: r.title,
182      snippet: (r.snippet || '').replace(/<[^>]+>/g, '').trim(),
183      url: `https://en.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/\s+/g, '_'))}`
184    }));
185  } catch (_) {
186    clearTimeout(timer);
187    return null;
188  }
189}
190
191/**
192 * Orchestrates direct client-side grounding:
193 * 1. Checks in-memory cache.
194 * 2. Queries DuckDuckGo JSONP.
195 * 3. Falls back or supplements with Wikipedia if needed.
196 * 4. Formats clean markdown context block for prompt injection.
197 * 
198 * @param {string} query 
199 * @returns {Promise<string|null>} Formatted grounding text, or null if nothing found.
200 */
201export async function getLiveGrounding(query) {
202  if (!query || typeof query !== 'string') return null;
203  const cleanKey = query.trim().toLowerCase();
204
205  // Check cache
206  const cached = searchCache.get(cleanKey);
207  if (cached && (Date.now() - cached.timestamp < CACHE_TTL_MS)) {
208    return cached.content;
209  }
210
211  try {
212    // Run DDG JSONP direct search
213    const ddgResult = await searchDuckDuckGoClient(cleanKey, 1500);
214    let facts = [];
215    let sources = [];
216
217    if (ddgResult) {
218      if (ddgResult.heading) facts.push(`Subject: ${ddgResult.heading}`);
219      if (ddgResult.answer) facts.push(`Instant Answer: ${ddgResult.answer}`);
220      if (ddgResult.abstract) facts.push(`Summary: ${ddgResult.abstract}`);
221      if (ddgResult.definition) facts.push(`Definition: ${ddgResult.definition}`);
222      if (ddgResult.url) sources.push(ddgResult.url);
223
224      for (const t of (ddgResult.topics || [])) {
225        facts.push(`- ${t.text}`);
226        if (t.url && !sources.includes(t.url)) sources.push(t.url);
227      }
228    }
229
230    // If DDG returned insufficient details, supplement with Wikipedia
231    if (facts.length < 2) {
232      const wikiResults = await searchWikipediaClient(cleanKey, 1200);
233      if (wikiResults && wikiResults.length > 0) {
234        for (const w of wikiResults) {
235          facts.push(`[Wikipedia: ${w.title}] ${w.snippet}`);
236          if (w.url && !sources.includes(w.url)) sources.push(w.url);
237        }
238      }
239    }
240
241    if (facts.length === 0) {
242      return null;
243    }
244
245    const todayStr = new Date().toLocaleDateString('en-US', {
246      weekday: 'long',
247      year: 'numeric',
248      month: 'long',
249      day: 'numeric'
250    });
251
252    const formattedContext = [
253      `[Real-Time Verified Knowledge | Current Date: ${todayStr}]`,
254      facts.join('\n'),
255      sources.length > 0 ? `Verified Sources: ${sources.join(', ')}` : '',
256      `[Instruction: Use the current facts above to give an accurate, up-to-date answer. Cite sources if appropriate.]`
257    ].filter(Boolean).join('\n\n');
258
259    // Store in cache
260    searchCache.set(cleanKey, {
261      content: formattedContext,
262      timestamp: Date.now()
263    });
264
265    return formattedContext;
266  } catch (err) {
267    console.warn('[ClientSearch] Grounding error (skipped):', err);
268    return null;
269  }
270}

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.