PageSourceSearch

https://www.splurjj.com/js/muse-search.js?v=1

js splurjj.com collected 2026-10-07 17:51:53 UTC 33,465 bytes, 715 lines download raw bytes

1/**
2 * muse-search.js  v1  —  Muse AI Search Assistant for Splurjj.com
3 * ──────────────────────────────────────────────────────────────────────────────
4 * Muse AI is trained EXCLUSIVELY on Splurjj.com content:
5 *   - GoalGraph knowledge graph (gg_entities, gg_milestones)
6 *   - Splurjj brand directory (dir_brands, dir_products)
7 *   - Splurjj editorial articles (cms_articles)
8 *   - Goals database (dir_goals)
9 *   - Local BRAND_DATABASE (10 brand-data files)
10 *
11 * Muse AI answers questions about Splurjj content ONLY.
12 * It does NOT answer general web queries or knowledge outside Splurjj.
13 *
14 * Architecture:
15 *   - query() → finds relevant content from Splurjj sources → builds context → returns AI answer
16 *   - getContext() → returns structured context about a query for display
17 *   - suggest() → smart suggestions based on Splurjj content
18 *   - chat() → conversational interface grounded in Splurjj data
19 *
20 * Public API:  window.MuseSearch
21 *   .query(q)           → Promise<{answer, sources, confidence}>
22 *   .getContext(q, results) → Promise<{summary, suggestions, relatedTopics}>
23 *   .suggest(q)         → Promise<string[]> — smart search suggestions
24 *   .chat(messages)     → Promise<{reply, sources}>
25 *   .askAboutBrand(name)    → Promise<{brand, answer}>
26 *   .askAboutPerson(name)   → Promise<{entity, answer}>
27 *   .askAboutGoal(title)    → Promise<{goal, answer}>
28 * ──────────────────────────────────────────────────────────────────────────────
29 */
30window.MuseSearch = (function () {
31  'use strict';
32
33  var SB_URL  = 'https://mpwvgqeqemdnipoogccv.supabase.co';
34  var SB_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1wd3ZncWVxZW1kbmlwb29nY2N2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzgxOTE4NzcsImV4cCI6MjA5Mzc2Nzg3N30.8bvaKPS3fXGjJH2XsUY9eHn7EIHxzLkhZKCKk7qCYKQ';
35
36  /* ── Supabase fetch ─────────────────────────────────────────────────────── */
37  async function sbGet(table, qs, limit) {
38    try {
39      var url = SB_URL + '/rest/v1/' + table + '?' + qs + '&limit=' + (limit || 10);
40      var r = await fetch(url, {
41        headers: { 'apikey': SB_ANON, 'Authorization': 'Bearer ' + SB_ANON, 'Accept': 'application/json' }
42      });
43      if (!r.ok) return [];
44      return await r.json();
45    } catch (e) { return []; }
46  }
47
48  /* ═══════════════════════════════════════════════════════════════════════════
49   *  SPLURJJ KNOWLEDGE RETRIEVAL
50   *  Gathers structured facts from all Splurjj sources
51   * ═════════════════════════════════════════════════════════════════════════ */
52  async function retrieveSplurjjKnowledge(q) {
53    var enc = encodeURIComponent(q);
54    var knowledge = { brands: [], entities: [], articles: [], goals: [], products: [] };
55
56    var [brands, entities, articles, goals, products] = await Promise.allSettled([
57      sbGet('dir_brands',
58        'brand_name=ilike.*' + enc + '*&select=id,brand_name,category,subcategory,founder,description,headquarters,website,tier,verification_status,trust_score',
59        5),
60      sbGet('gg_entities',
61        'or=(name.ilike.*' + enc + '*,summary.ilike.*' + enc + '*,known_for.ilike.*' + enc + '*)&status=eq.active&select=id,name,type,category,summary,known_for,nationality,slug,trust_score,is_verified',
62        8),
63      sbGet('cms_articles',
64        'title=ilike.*' + enc + '*&select=id,title,subtitle,category_name,author_name,slug,published_at',
65        5),
66      sbGet('dir_goals',
67        'or=(goal_title.ilike.*' + enc + '*,description.ilike.*' + enc + '*)&select=*',
68        5),
69      sbGet('dir_products',
70        'product_name=ilike.*' + enc + '*&select=*',
71        5)
72    ]);
73
74    knowledge.brands   = brands.status   === 'fulfilled' ? brands.value   : [];
75    knowledge.entities = entities.status === 'fulfilled' ? entities.value : [];
76    knowledge.articles = articles.status === 'fulfilled' ? articles.value : [];
77    knowledge.goals    = goals.status    === 'fulfilled' ? goals.value    : [];
78    knowledge.products = products.status === 'fulfilled' ? products.value : [];
79
80    // Also check local BRAND_DATABASE
81    if (typeof BRAND_DATABASE !== 'undefined' && knowledge.brands.length < 3) {
82      var qLow = q.toLowerCase();
83      Object.values(BRAND_DATABASE).forEach(function(cat) {
84        (cat.brands || []).forEach(function(b) {
85          if ((b.name || '').toLowerCase().indexOf(qLow) !== -1) {
86            knowledge.brands.push({
87              brand_name: b.name,
88              category:   cat.label || '',
89              website:    b.website || '',
90              tier:       b.tier || 2,
91              description: b.description || '',
92              _src: 'local'
93            });
94          }
95        });
96      });
97      knowledge.brands = knowledge.brands.slice(0, 5);
98    }
99
100    return knowledge;
101  }
102
103  /* ═══════════════════════════════════════════════════════════════════════════
104   *  KNOWLEDGE FORMATTER
105   *  Converts raw Supabase data into readable context strings
106   * ═════════════════════════════════════════════════════════════════════════ */
107  function formatKnowledge(k) {
108    var parts = [];
109
110    if (k.entities && k.entities.length) {
111      k.entities.forEach(function(e) {
112        var line = '• ' + e.name + (e.type ? ' [' + e.type + ']' : '');
113        if (e.category) line += ' — ' + e.category;
114        if (e.known_for) line += '. Known for: ' + e.known_for;
115        if (e.summary) line += '. ' + e.summary.substring(0, 150);
116        if (e.nationality) line += ' (' + e.nationality + ')';
117        parts.push(line);
118      });
119    }
120
121    if (k.brands && k.brands.length) {
122      k.brands.forEach(function(b) {
123        var line = '• ' + b.brand_name + ' [brand]';
124        if (b.category) line += ' — ' + b.category;
125        if (b.founder) line += '. Founded by ' + b.founder;
126        if (b.headquarters) line += '. HQ: ' + b.headquarters;
127        if (b.description) line += '. ' + b.description.substring(0, 100);
128        parts.push(line);
129      });
130    }
131
132    if (k.articles && k.articles.length) {
133      k.articles.forEach(function(a) {
134        var line = '• Article: "' + a.title + '"';
135        if (a.subtitle) line += ' — ' + a.subtitle;
136        if (a.category_name) line += ' [' + a.category_name + ']';
137        if (a.author_name) line += ' by ' + a.author_name;
138        parts.push(line);
139      });
140    }
141
142    if (k.goals && k.goals.length) {
143      k.goals.forEach(function(g) {
144        var line = '• Goal: "' + (g.goal_title || g.title || '') + '"';
145        if (g.description) line += ' — ' + (g.description || '').substring(0, 100);
146        parts.push(line);
147      });
148    }
149
150    if (k.products && k.products.length) {
151      k.products.forEach(function(p) {
152        var line = '• Product: ' + (p.product_name || p.name);
153        if (p.brand_name) line += ' by ' + p.brand_name;
154        parts.push(line);
155      });
156    }
157
158    return parts.join('\n');
159  }
160
161  /* ═══════════════════════════════════════════════════════════════════════════
162   *  MUSE AI RESPONSE ENGINE
163   *  Generates contextual answers grounded in Splurjj data
164   *  Uses rule-based AI (no external LLM needed — Splurjj data is the model)
165   * ═════════════════════════════════════════════════════════════════════════ */
166  function generateMuseAnswer(q, knowledge) {
167    var qLow = q.toLowerCase().trim();
168    var sources = [];
169    var answer  = '';
170    var confidence = 0;
171
172    // Determine query intent
173    var isWhatIs   = /^what (is|are|was|were)\b/i.test(q);
174    var isWhoIs    = /^who (is|was|are|were)\b/i.test(q);
175    var isFindBrand = /brand|company|companies/i.test(q);
176    var isArticle   = /article|read|blog|news|story/i.test(q);
177    var isGoal      = /goal|achieve|how to|want to/i.test(q);
178
179    // Priority: GoalGraph entities (richest data)
180    if (knowledge.entities && knowledge.entities.length) {
181      var top = knowledge.entities[0];
182      confidence = Math.min(95, 60 + (knowledge.entities.length * 5));
183
184      if (isWhoIs && top.type === 'person') {
185        answer = top.name + ' is a ' + (top.nationality ? top.nationality + ' ' : '') + (top.category || 'notable person');
186        if (top.known_for) answer += ' known for ' + top.known_for;
187        if (top.summary)   answer += '. ' + top.summary.substring(0, 200);
188      } else if (isWhatIs && top.type === 'company') {
189        answer = top.name + ' is ' + (top.category ? 'a ' + top.category + ' company' : 'a company');
190        if (top.known_for) answer += ', known for ' + top.known_for;
191        if (top.summary)   answer += '. ' + top.summary.substring(0, 200);
192      } else if (top.summary) {
193        answer = top.name + ': ' + top.summary.substring(0, 250);
194      } else {
195        answer = 'Found "' + top.name + '" in the GoalGraph — ' + (top.category || top.type || 'entity') + (top.known_for ? ', known for ' + top.known_for : '');
196      }
197
198      knowledge.entities.forEach(function(e) {
199        sources.push({ type: 'goalgraph', label: e.name, url: 'goalgraph-entity.html?id=' + e.id });
200      });
201    }
202
203    // Add brand context
204    if (knowledge.brands && knowledge.brands.length) {
205      var topBrand = knowledge.brands[0];
206      if (!answer) {
207        answer = topBrand.brand_name + ' is ' + (topBrand.category ? 'a ' + topBrand.category + ' brand' : 'a brand');
208        if (topBrand.founder)      answer += ', founded by ' + topBrand.founder;
209        if (topBrand.headquarters) answer += ', headquartered in ' + topBrand.headquarters;
210        if (topBrand.description)  answer += '. ' + topBrand.description.substring(0, 200);
211        confidence = Math.max(confidence, 75);
212      } else if (knowledge.brands.length > 0) {
213        answer += '\n\nRelated brands: ' + knowledge.brands.slice(0, 3).map(function(b){ return b.brand_name; }).join(', ');
214      }
215      knowledge.brands.forEach(function(b) {
216        sources.push({ type: 'brand', label: b.brand_name, url: b.website || 'brand-profile.html?id=' + (b.id || b.splurjj_id || '') });
217      });
218    }
219
220    // Add article context
221    if (knowledge.articles && knowledge.articles.length) {
222      if (!answer) {
223        var art = knowledge.articles[0];
224        answer = 'Found an article about this on Splurjj: "' + art.title + '"';
225        if (art.subtitle) answer += ' — ' + art.subtitle;
226        confidence = Math.max(confidence, 65);
227      } else {
228        answer += '\n\nRelated articles on Splurjj: ' + knowledge.articles.slice(0, 2).map(function(a){ return '"' + a.title + '"'; }).join(', ');
229      }
230      knowledge.articles.forEach(function(a) {
231        sources.push({ type: 'article', label: a.title, url: 'article.html?slug=' + a.slug });
232      });
233    }
234
235    // Add goal context
236    if (knowledge.goals && knowledge.goals.length) {
237      var topGoal = knowledge.goals[0];
238      if (!answer) {
239        answer = 'Goal found on Splurjj: "' + (topGoal.goal_title || topGoal.title) + '"';
240        if (topGoal.description) answer += '. ' + topGoal.description.substring(0, 150);
241        confidence = Math.max(confidence, 70);
242      }
243      knowledge.goals.forEach(function(g) {
244        sources.push({ type: 'goal', label: g.goal_title || g.title, url: 'goals.html?id=' + g.id });
245      });
246    }
247
248    // Fallback: nothing found on Splurjj
249    if (!answer) {
250      answer = 'I couldn\'t find specific information about "' + q + '" in Splurjj\'s database. Try browsing the GoalGraph or searching by category.';
251      confidence = 10;
252    }
253
254    return {
255      answer:     answer,
256      sources:    sources,
257      confidence: confidence,
258      knowledge:  knowledge
259    };
260  }
261
262  /* ═══════════════════════════════════════════════════════════════════════════
263   *  QUERY — main entry point
264   * ═════════════════════════════════════════════════════════════════════════ */
265  async function query(q) {
266    if (!q || !q.trim()) return { answer: '', sources: [], confidence: 0 };
267    var knowledge = await retrieveSplurjjKnowledge(q.trim());
268    return generateMuseAnswer(q.trim(), knowledge);
269  }
270
271  /* ═══════════════════════════════════════════════════════════════════════════
272   *  GET CONTEXT — returns structured context to enrich search results
273   *  Suggestions are REAL entity/brand/article names from Splurjj data,
274   *  NOT raw category strings.
275   * ═════════════════════════════════════════════════════════════════════════ */
276  async function getContext(q, topResults) {
277    if (!q) return null;
278    topResults = topResults || [];
279
280    // Build related topics from actual result types (not category text)
281    var typeCounts = {};
282    topResults.forEach(function(r) {
283      var t = r._type || '';
284      if (t) typeCounts[t] = (typeCounts[t] || 0) + 1;
285    });
286    var relatedTopics = Object.keys(typeCounts)
287      .sort(function(a,b){ return typeCounts[b] - typeCounts[a]; })
288      .slice(0, 5);
289
290    // Smart suggestions: real names from results + Supabase
291    var suggestions = await buildSuggestions(q, topResults);
292
293    // Summary built from actual top result names
294    var summary = '';
295    if (topResults.length === 0) {
296      summary = 'No results found for "' + q + '" — try a broader term or browse by category.';
297    } else {
298      var topName = topResults[0].brand_name || topResults[0].name || topResults[0].title || topResults[0].goal_title || q;
299      summary = topResults.length + ' result' + (topResults.length !== 1 ? 's' : '') + ' for "' + q + '"';
300      if (topName && topName.toLowerCase() !== q.toLowerCase()) summary += ' — top: ' + topName;
301      if (relatedTopics.length > 1) summary += ' across ' + relatedTopics.slice(0, 3).join(', ');
302    }
303
304    return {
305      summary:       summary,
306      suggestions:   suggestions,
307      relatedTopics: relatedTopics,
308      resultCount:   topResults.length
309    };
310  }
311
312  /* ── Smart suggestion builder — returns REAL names from Splurjj data ──────── */
313  async function buildSuggestions(q, results) {
314    var suggestions = [];
315    var qLow = q.toLowerCase();
316    var seen = {};
317    seen[qLow] = true; // never suggest the exact same query
318
319    function add(text) {
320      if (!text || text.length < 2) return;
321      var key = text.toLowerCase();
322      if (seen[key]) return;
323      seen[key] = true;
324      suggestions.push(text);
325    }
326
327    // 1. Names from top search results (entities, brands, articles, goals)
328    results.forEach(function(r) {
329      add(r.name || '');
330      add(r.brand_name || '');
331      add(r.title || '');
332      add(r.goal_title || '');
333    });
334
335    // 2. Query Supabase for related names (starts-with fallback to contains)
336    var enc = encodeURIComponent(q);
337    try {
338      var [relBrands, relEntities, relArticles, relGoals] = await Promise.allSettled([
339        sbGet('dir_brands',
340          'brand_name=ilike.' + enc + '*&select=brand_name,category', 6),
341        sbGet('gg_entities',
342          'name=ilike.' + enc + '*&status=eq.active&select=name,type,category', 6),
343        sbGet('cms_articles',
344          'title=ilike.*' + enc + '*&select=title,category_name&limit=4', 4),
345        sbGet('dir_goals',
346          'goal_title=ilike.*' + enc + '*&select=goal_title&limit=3', 3)
347      ]);
348      (relBrands.status   === 'fulfilled' ? relBrands.value   : []).forEach(function(b){ add(b.brand_name); });
349      (relEntities.status === 'fulfilled' ? relEntities.value : []).forEach(function(e){ add(e.name); });
350      (relArticles.status === 'fulfilled' ? relArticles.value : []).forEach(function(a){ add(a.title); });
351      (relGoals.status    === 'fulfilled' ? relGoals.value    : []).forEach(function(g){ add(g.goal_title); });
352    } catch(e) { /* silent — Supabase may not be reachable */ }
353
354    // 3. Contextual expansions for common themes — appended ONLY if < 4 suggestions so far
355    if (suggestions.length < 4) {
356      var expansions = {
357        'nike':     ['Nike Air Max', 'Nike goals', 'Nike sportswear'],
358        'apple':    ['Apple iPhone', 'Apple MacBook', 'Apple goals'],
359        'fashion':  ['luxury fashion brands', 'streetwear brands', 'fashion goals'],
360        'tech':     ['tech brands', 'big tech companies', 'tech skills'],
361        'music':    ['music artists', 'music goals', 'music brands'],
362        'fitness':  ['fitness goals', 'fitness brands', 'workout habits'],
363        'sneaker':  ['sneaker brands', 'Jordan', 'Adidas'],
364        'car':      ['luxury cars', 'electric cars', 'car brands'],
365        'style':    ['style goals', 'luxury style', 'streetwear'],
366        'brand':    ['luxury brands', 'tech brands', 'fashion brands'],
367        'goal':     ['fitness goals', 'career goals', 'lifestyle goals'],
368        'book':     ['must-read books', 'book goals', 'bestseller books'],
369        'film':     ['must-watch films', 'film goals', 'award-winning films'],
370        'song':     ['music goals', 'trending songs', 'music artists'],
371        'habit':    ['daily habits', 'productivity habits', 'fitness habits'],
372        'skill':    ['coding skills', 'leadership skills', 'creative skills'],
373        'location': ['travel goals', 'best cities', 'travel brands']
374      };
375      Object.keys(expansions).forEach(function(keyword) {
376        if (qLow.indexOf(keyword) !== -1 || keyword.indexOf(qLow) !== -1) {
377          expansions[keyword].forEach(function(exp) { add(exp); });
378        }
379      });
380    }
381
382    // 4. KEYWORD_ALIASES expansions
383    if (window.SplurjjSearch && window.SplurjjSearch.KEYWORD_ALIASES) {
384      Object.keys(window.SplurjjSearch.KEYWORD_ALIASES).forEach(function(alias) {
385        if (alias !== qLow && (alias.indexOf(qLow) !== -1 || qLow.indexOf(alias) !== -1)) {
386          add(alias);
387        }
388      });
389    }
390
391    return suggestions.slice(0, 6);
392  }
393
394  /* ═══════════════════════════════════════════════════════════════════════════
395   *  SUGGEST — returns smart autocomplete suggestions based on Splurjj content
396   * ═════════════════════════════════════════════════════════════════════════ */
397  async function suggest(q) {
398    if (!q || q.length < 1) return [];
399    var enc = encodeURIComponent(q);
400    var [brands, entities] = await Promise.allSettled([
401      sbGet('dir_brands', 'brand_name=ilike.' + enc + '*&select=brand_name,category', 5),
402      sbGet('gg_entities', 'name=ilike.' + enc + '*&status=eq.active&select=name,type,category', 5)
403    ]);
404    var suggestions = [];
405    var seen = {};
406
407    (brands.status === 'fulfilled' ? brands.value : []).forEach(function(b) {
408      if (b.brand_name && !seen[b.brand_name]) {
409        seen[b.brand_name] = true;
410        suggestions.push({ text: b.brand_name, type: 'brand', sublabel: b.category || 'Brand' });
411      }
412    });
413    (entities.status === 'fulfilled' ? entities.value : []).forEach(function(e) {
414      if (e.name && !seen[e.name]) {
415        seen[e.name] = true;
416        suggestions.push({ text: e.name, type: e.type || 'entity', sublabel: e.category || e.type || '' });
417      }
418    });
419
420    return suggestions.slice(0, 10);
421  }
422
423  /* ═══════════════════════════════════════════════════════════════════════════
424   *  CHAT — conversational AI grounded in Splurjj data
425   * ═════════════════════════════════════════════════════════════════════════ */
426  async function chat(messages) {
427    if (!messages || !messages.length) return { reply: '', sources: [] };
428
429    // Get the latest user message
430    var lastMsg = messages[messages.length - 1];
431    var userText = lastMsg.content || '';
432
433    // Retrieve knowledge for this message
434    var knowledge = await retrieveSplurjjKnowledge(userText);
435    var result = generateMuseAnswer(userText, knowledge);
436
437    // Build conversational reply
438    var reply = result.answer;
439
440    // Add follow-up suggestion
441    if (result.sources && result.sources.length) {
442      reply += '\n\n_You can explore more on GoalGraph or the Splurjj Brand Directory._';
443    }
444
445    return {
446      reply:      reply,
447      sources:    result.sources,
448      confidence: result.confidence,
449      knowledge:  formatKnowledge(knowledge)
450    };
451  }
452
453  /* ═══════════════════════════════════════════════════════════════════════════
454   *  ASK ABOUT BRAND — deep dive into a brand from all Splurjj sources
455   * ═════════════════════════════════════════════════════════════════════════ */
456  async function askAboutBrand(brandName) {
457    var enc = encodeURIComponent(brandName);
458    var [dirBrand, ggEntity] = await Promise.allSettled([
459      sbGet('dir_brands', 'brand_name=ilike.*' + enc + '*&select=*', 1),
460      sbGet('gg_entities', 'name=ilike.*' + enc + '*&type=eq.company&status=eq.active&select=*', 1)
461    ]);
462
463    var brand  = (dirBrand.status  === 'fulfilled' ? dirBrand.value  : [])[0] || null;
464    var entity = (ggEntity.status  === 'fulfilled' ? ggEntity.value  : [])[0] || null;
465
466    // Also check local
467    if (!brand && typeof BRAND_DATABASE !== 'undefined') {
468      var bLow = brandName.toLowerCase();
469      Object.values(BRAND_DATABASE).some(function(cat) {
470        return (cat.brands || []).some(function(b) {
471          if ((b.name || '').toLowerCase() === bLow) {
472            brand = { brand_name: b.name, category: cat.label, website: b.website, tags: b.tags, tier: b.tier, description: b.description, _src: 'local' };
473            return true;
474          }
475        });
476      });
477    }
478
479    var answer = '';
480    if (brand) {
481      answer = brand.brand_name + ' is a ' + (brand.category || 'brand');
482      if (brand.founder)      answer += ', founded by ' + brand.founder;
483      if (brand.headquarters) answer += ', based in ' + brand.headquarters;
484      if (brand.description)  answer += '. ' + brand.description.substring(0, 300);
485      if (brand.website)      answer += ' Website: ' + brand.website;
486    } else if (entity) {
487      answer = entity.name + ': ' + (entity.summary || entity.known_for || 'Company in GoalGraph');
488    } else {
489      answer = 'Could not find "' + brandName + '" in Splurjj\'s brand directory.';
490    }
491
492    return { brand: brand, entity: entity, answer: answer };
493  }
494
495  /* ═══════════════════════════════════════════════════════════════════════════
496   *  ASK ABOUT PERSON — deep dive into a person from GoalGraph
497   * ═════════════════════════════════════════════════════════════════════════ */
498  async function askAboutPerson(personName) {
499    var enc = encodeURIComponent(personName);
500    var rows = await sbGet('gg_entities',
501      'name=ilike.*' + enc + '*&type=eq.person&status=eq.active&select=*', 3);
502
503    var entity = (rows || [])[0] || null;
504    var answer = '';
505
506    if (entity) {
507      answer = entity.name;
508      if (entity.nationality) answer += ' is a ' + entity.nationality + ' person';
509      if (entity.category)    answer += ' in ' + entity.category;
510      if (entity.known_for)   answer += ', known for ' + entity.known_for;
511      if (entity.summary)     answer += '. ' + entity.summary.substring(0, 300);
512    } else {
513      answer = 'Could not find "' + personName + '" in the GoalGraph knowledge base.';
514    }
515
516    return { entity: entity, answer: answer };
517  }
518
519  /* ═══════════════════════════════════════════════════════════════════════════
520   *  ASK ABOUT GOAL
521   * ═════════════════════════════════════════════════════════════════════════ */
522  async function askAboutGoal(goalTitle) {
523    var enc = encodeURIComponent(goalTitle);
524    var rows = await sbGet('dir_goals',
525      'or=(goal_title.ilike.*' + enc + '*,description.ilike.*' + enc + '*)&select=*', 3);
526
527    var goal = (rows || [])[0] || null;
528    var answer = '';
529
530    if (goal) {
531      answer = 'Goal: "' + (goal.goal_title || goal.title) + '"';
532      if (goal.description) answer += '. ' + goal.description.substring(0, 300);
533    } else {
534      answer = 'Could not find a goal matching "' + goalTitle + '" in Splurjj\'s goals database.';
535    }
536
537    return { goal: goal, answer: answer };
538  }
539
540  /* ═══════════════════════════════════════════════════════════════════════════
541   *  MUSE AI PANEL RENDERER
542   *  Renders the Muse AI response into a DOM element
543   * ═════════════════════════════════════════════════════════════════════════ */
544  function renderMusePanel(containerId, queryResult) {
545    var el = document.getElementById(containerId);
546    if (!el) return;
547
548    var r = queryResult;
549    var confClass = r.confidence >= 80 ? 'muse-conf-high' : (r.confidence >= 50 ? 'muse-conf-mid' : 'muse-conf-low');
550
551    var sourcesHtml = '';
552    if (r.sources && r.sources.length) {
553      sourcesHtml = '<div class="muse-sources">' +
554        '<span class="muse-sources-label">Sources on Splurjj:</span>' +
555        r.sources.slice(0,4).map(function(s) {
556          return '<a href="' + s.url + '" class="muse-source-chip">' +
557            '<i class="fas ' + (s.type === 'brand' ? 'fa-tag' : s.type === 'article' ? 'fa-newspaper' : s.type === 'goal' ? 'fa-bullseye' : 'fa-network-wired') + '"></i> ' +
558            s.label +
559          '</a>';
560        }).join('') +
561      '</div>';
562    }
563
564    el.innerHTML =
565      '<div class="muse-panel">' +
566        '<div class="muse-header">' +
567          '<span class="muse-logo"><i class="fas fa-brain"></i> Muse AI</span>' +
568          '<span class="muse-badge ' + confClass + '">' + r.confidence + '% match</span>' +
569        '</div>' +
570        '<div class="muse-answer">' + r.answer.replace(/\n/g, '<br>') + '</div>' +
571        sourcesHtml +
572        '<div class="muse-footer">Muse AI searches only Splurjj.com content &amp; GoalGraph</div>' +
573      '</div>';
574  }
575
576  /* ═══════════════════════════════════════════════════════════════════════════
577   *  MUSE AI PANEL CSS  (injected once)
578   * ═════════════════════════════════════════════════════════════════════════ */
579  (function injectMuseStyles() {
580    if (document.getElementById('muse-search-styles')) return;
581    var s = document.createElement('style');
582    s.id = 'muse-search-styles';
583    s.textContent = `
584      .muse-panel {
585        background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
586        border: 1px solid rgba(251,191,36,0.25);
587        border-radius: 16px;
588        padding: 18px 20px;
589        margin: 12px 0;
590        font-family: 'Inter', system-ui, sans-serif;
591        position: relative;
592        overflow: hidden;
593      }
594      .muse-panel::before {
595        content: '';
596        position: absolute;
597        top: 0; left: 0; right: 0;
598        height: 2px;
599        background: linear-gradient(90deg, #fbbf24, #f59e0b, #d97706);
600      }
601      .muse-header {
602        display: flex;
603        align-items: center;
604        gap: 12px;
605        margin-bottom: 12px;
606      }
607      .muse-logo {
608        font-size: 13px;
609        font-weight: 700;
610        color: #fbbf24;
611        letter-spacing: 0.5px;
612        display: flex;
613        align-items: center;
614        gap: 6px;
615      }
616      .muse-logo i { font-size: 14px; }
617      .muse-badge {
618        font-size: 11px;
619        padding: 2px 8px;
620        border-radius: 20px;
621        font-weight: 600;
622        margin-left: auto;
623      }
624      .muse-conf-high { background: rgba(34,197,94,0.2); color: #86efac; }
625      .muse-conf-mid  { background: rgba(234,179,8,0.2);  color: #fef08a; }
626      .muse-conf-low  { background: rgba(148,163,184,0.15); color: #94a3b8; }
627      .muse-answer {
628        color: #e2e8f0;
629        font-size: 14px;
630        line-height: 1.65;
631        margin-bottom: 14px;
632      }
633      .muse-sources {
634        display: flex;
635        flex-wrap: wrap;
636        gap: 6px;
637        margin-bottom: 12px;
638        align-items: center;
639      }
640      .muse-sources-label {
641        font-size: 11px;
642        color: #64748b;
643        font-weight: 600;
644        text-transform: uppercase;
645        letter-spacing: 0.5px;
646        width: 100%;
647        margin-bottom: 4px;
648      }
649      .muse-source-chip {
650        display: inline-flex;
651        align-items: center;
652        gap: 5px;
653        padding: 4px 10px;
654        background: rgba(255,255,255,0.06);
655        border: 1px solid rgba(255,255,255,0.1);
656        border-radius: 20px;
657        font-size: 12px;
658        color: #cbd5e1;
659        text-decoration: none;
660        transition: all 0.15s;
661      }
662      .muse-source-chip:hover {
663        background: rgba(251,191,36,0.1);
664        border-color: rgba(251,191,36,0.3);
665        color: #fbbf24;
666      }
667      .muse-footer {
668        font-size: 10px;
669        color: #475569;
670        font-style: italic;
671      }
672      /* Modal Muse AI section */
673      .splModal_museSection {
674        border-top: 1px solid rgba(255,255,255,0.06);
675        margin-top: 8px;
676        padding-top: 8px;
677      }
678      .splModal_museToggle {
679        display: flex;
680        align-items: center;
681        gap: 8px;
682        padding: 8px 14px;
683        cursor: pointer;
684        color: #fbbf24;
685        font-size: 13px;
686        font-weight: 600;
687        transition: opacity 0.15s;
688        border-radius: 8px;
689        background: rgba(251,191,36,0.08);
690        border: 1px solid rgba(251,191,36,0.15);
691        margin: 0 16px 8px;
692      }
693      .splModal_museToggle:hover { opacity: 0.85; }
694      .splModal_museToggle i { font-size: 14px; }
695      .splModal_musePanel { padding: 0 16px 12px; }
696    `;
697    document.head.appendChild(s);
698  })();
699
700  /* ── Public API ─────────────────────────────────────────────────────────── */
701  return {
702    query:           query,
703    getContext:      getContext,
704    suggest:         suggest,
705    chat:            chat,
706    askAboutBrand:   askAboutBrand,
707    askAboutPerson:  askAboutPerson,
708    askAboutGoal:    askAboutGoal,
709    renderMusePanel: renderMusePanel,
710    // Internal helpers exposed for extensibility
711    retrieveSplurjjKnowledge: retrieveSplurjjKnowledge,
712    formatKnowledge:          formatKnowledge
713  };
714
715})();

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.