AiCoderv2 commited on
Commit
78505c8
·
verified ·
1 Parent(s): 4261cb3

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +364 -196
index.html CHANGED
@@ -3,9 +3,9 @@
3
 
4
  <head>
5
  <meta charset="UTF-8" />
6
- <title>DuckDuckGo AI Chatbot</title>
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
- <meta name="description" content="A simple chatbot that answers using DuckDuckGo instant answers." />
9
  <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
@@ -304,6 +304,11 @@
304
  border-radius: 8px;
305
  }
306
 
 
 
 
 
 
307
  .composer {
308
  position: sticky;
309
  bottom: 0;
@@ -453,6 +458,40 @@
453
  padding: 10px 0 18px;
454
  }
455
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  /* Responsive */
457
  @media (max-width: 720px) {
458
  .topbar {
@@ -518,6 +557,42 @@
518
  padding: 10px;
519
  overflow: auto;
520
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  </style>
522
  </head>
523
 
@@ -528,8 +603,8 @@
528
  <div class="brand">
529
  <div class="logo" aria-hidden="true"></div>
530
  <div class="titles">
531
- <div class="title">DuckDuckGo Answer Bot</div>
532
- <div class="subtitle">Real-time answers via DuckDuckGo Instant Answers API (CORS-proxied).</div>
533
  </div>
534
  </div>
535
  <div class="links">
@@ -541,6 +616,12 @@
541
  </svg>
542
  DuckDuckGo
543
  </a>
 
 
 
 
 
 
544
  <button id="exportBtn" class="btn secondary" title="Export chat as JSON">
545
  <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
546
  Export
@@ -565,13 +646,11 @@
565
  <div class="bubble bot">
566
  <div class="meta">
567
  <span class="badge">Bot</span>
568
- <span>DuckDuckGo Instant Answers</span>
569
  </div>
570
  <div class="answer">
571
- <p>Hi! Im your DuckDuckGo-powered assistant. Ask me anything: facts, definitions, unit conversions,
572
- weather, time, math, and more.</p>
573
- <p class="hint">Tip: Try queries like “weather in Tokyo”, “define ubiquitous”, “2pm PST to CET”, or
574
- “what is 18 celsius in fahrenheit”.</p>
575
  </div>
576
  </div>
577
  </div>
@@ -596,7 +675,7 @@
596
  </main>
597
 
598
  <div class="footer-note">
599
- Answers come from DuckDuckGo Instant Answers. CORS is bypassed via public proxies for in-browser usage.
600
  </div>
601
  </div>
602
 
@@ -622,10 +701,12 @@
622
  let history = [];
623
  let abortController = null;
624
 
625
- // Suggestions
626
  const defaultChips = [
627
- 'weather in Tokyo', 'time in London', 'define ubiquitous', '2pm PST to CET', '18 celsius to fahrenheit',
628
- 'who is Ada Lovelace', 'sqrt(144)', 'USD to EUR', 'prime factors of 91'
 
 
629
  ];
630
 
631
  function renderSuggestions(items=defaultChips){
@@ -646,12 +727,14 @@
646
  function buildPromptChips(q){
647
  q = q.trim();
648
  const chips = [];
649
- const hasWeather = /\b(weather|temperature)\b/i.test(q);
650
  const hasDefine = /\b(define|meaning|definition|what does)\b/i.test(q);
651
  const hasConvert = /\b(to|in|°)\b/i.test(q) || /\b(c|f|cm|inch|kg|lb)\b/.test(q);
652
  const hasTime = /\b(time|datetime|date)\b/i.test(q);
653
  const hasWho = /\b(who is|who's|who’re)\b/i.test(q);
654
  const hasCalc = /[\d\+\-\*\/\^\(\)\.]/.test(q);
 
 
655
 
656
  if (q && !hasWeather) chips.push(q.replace(/\b(what is|what's)\b/i,'').trim() + ' weather');
657
  if (q && !hasDefine) chips.push('define ' + q.replace(/\b(define|meaning|definition|what does)\b/i,'').trim());
@@ -659,6 +742,8 @@
659
  if (q && !hasTime) chips.push('time in ' + q.replace(/\b(what is|what's|time in|date|datetime)\b/ig,'').trim());
660
  if (q && !hasWho) chips.push('who is ' + q.replace(/\b(who is|who's|who’re)\b/ig,'').trim());
661
  if (q && !hasCalc) chips.push(q.replace(/\b(calculate|compute|solve)\b/i,'').trim());
 
 
662
 
663
  // de-dup
664
  const unique = [...new Set(chips)].filter(Boolean).slice(0,6);
@@ -682,8 +767,16 @@
682
  metaEl.className = 'meta';
683
  const badge = document.createElement('span');
684
  badge.className = 'badge';
685
- badge.textContent = role === 'bot' ? (meta.badge || 'DuckDuckGo') : 'You';
686
  metaEl.appendChild(badge);
 
 
 
 
 
 
 
 
687
  if (meta.note){
688
  const note = document.createElement('span');
689
  note.textContent = meta.note;
@@ -703,7 +796,7 @@
703
  sources.className = 'sources';
704
  meta.sources.forEach(src => {
705
  const s = document.createElement('a');
706
- s.className = 'source';
707
  s.href = src.href;
708
  s.target = '_blank';
709
  s.rel = 'noopener noreferrer';
@@ -733,7 +826,7 @@
733
  metaEl.className = 'meta';
734
  const badge = document.createElement('span');
735
  badge.className = 'badge';
736
- badge.textContent = 'DuckDuckGo';
737
  metaEl.appendChild(badge);
738
  const answer = document.createElement('div');
739
  answer.className = 'answer';
@@ -751,28 +844,13 @@
751
  if (t) t.remove();
752
  }
753
 
754
- // Build DuckDuckGo API URL
755
- function buildDDGUrl(query){
756
- const url = new URL('https://api.duckduckgo.com/');
757
- url.searchParams.set('q', query);
758
- url.searchParams.set('format', 'json');
759
- url.searchParams.set('no_html', '1');
760
- url.searchParams.set('no_redirect', '1');
761
- url.searchParams.set('skip_disambig', '1');
762
- return url.toString();
763
- }
764
-
765
- // CORS proxy list (tried in order). Using public proxies to bypass CORS in-browser.
766
  const CORS_PROXIES = [
767
- // https://cors.isomorphic-git.org/ simply adds permissive CORS headers to any URL
768
  (target) => `https://cors.isomorphic-git.org/${target}`,
769
- // https://api.codetabs.com/v1/proxy?quest= forwards the request server-side
770
  (target) => `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(target)}`,
771
- // https://corsproxy.io/? provides a minimal CORS proxy
772
  (target) => `https://corsproxy.io/?${encodeURIComponent(target)}`
773
  ];
774
 
775
- // Fetch with automatic CORS proxy fallback (real DuckDuckGo data only).
776
  async function fetchWithCorsProxies(targetUrl, signal){
777
  let lastErr = null;
778
  for (const buildProxy of CORS_PROXIES){
@@ -781,198 +859,288 @@
781
  const res = await fetch(proxyUrl, { signal, headers: { 'Accept': 'application/json' } });
782
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
783
  const contentType = res.headers.get('content-type') || '';
784
- // Some proxies may return text; we only accept JSON
785
  if (!contentType.includes('application/json') && !contentType.includes('text/plain')) {
786
  throw new Error('Unexpected content-type: ' + contentType);
787
  }
788
- // If it's text (e.g., JSON as text), still parse it
789
  const text = await res.text();
790
  const data = JSON.parse(text);
791
  return data;
792
  }catch(err){
793
  lastErr = err;
794
- // try next proxy
795
  }
796
  }
797
  throw lastErr || new Error('All CORS proxies failed');
798
  }
799
 
800
- // DuckDuckGo fetch (real data only, via CORS proxies)
801
- async function fetchDDG(query, signal){
802
- const targetUrl = buildDDGUrl(query);
803
- const data = await fetchWithCorsProxies(targetUrl, signal);
804
- return data;
 
 
 
805
  }
806
 
807
- // Parse DuckDuckGo result
808
- function parseDDG(data, originalQuery){
809
- const sources = [];
810
- if (Array.isArray(data.Results)) {
811
- data.Results.forEach(r => {
812
- if (r && r.FirstURL && r.Text) sources.push({ href: r.FirstURL, label: new URL(r.FirstURL).hostname.replace('www.','') });
813
- });
 
 
 
 
 
 
814
  }
815
- if (data.AbstractURL) sources.push({ href: data.AbstractURL, label: new URL(data.AbstractURL).hostname.replace('www.','') });
816
- if (data.Answer && data.AnswerURL) sources.push({ href: data.AnswerURL, label: new URL(data.AnswerURL).hostname.replace('www.','') });
817
-
818
- // Primary text
819
- let primary = '';
820
- let note = '';
821
-
822
- if (data.Answer && data.Answer.trim()){
823
- primary = data.Answer;
824
- note = 'Instant Answer';
825
- } else if (data.Definition && data.Definition.trim()){
826
- primary = data.Definition;
827
- note = data.DefinitionSource ? ('Definition ' + data.DefinitionSource) : 'Definition';
828
- if (data.DefinitionURL) sources.push({ href: data.DefinitionURL, label: new URL(data.DefinitionURL).hostname.replace('www.','') });
829
- } else if (data.AbstractText && data.AbstractText.trim()){
830
- primary = data.AbstractText;
831
- note = data.Heading ? ('About: ' + data.Heading) : (data.AbstractSource ? data.AbstractSource : 'Abstract');
832
- } else if (data.RelatedTopics && data.RelatedTopics.length){
833
- // Build a short list
834
- const items = [];
835
- for (const t of data.RelatedTopics){
836
- if (t && t.Text && t.FirstURL) {
837
- items.push(`<li><a href="${t.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(t.Text)}</a></li>`);
838
- } else if (Array.isArray(t.Topics)) {
839
- for (const tt of t.Topics){
840
- if (tt && tt.Text && tt.FirstURL) {
841
- items.push(`<li><a href="${tt.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(tt.Text)}</a></li>`);
 
 
 
 
 
 
 
 
 
 
842
  }
843
  }
 
 
 
 
 
844
  }
845
- if (items.length >= 6) break;
846
- }
847
- if (items.length){
848
- primary = `<p>Here are some related topics I found:</p><ul>${items.join('')}</ul>`;
849
- note = 'Related topics';
850
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
  }
852
 
853
- if (!primary){
854
- // Disambiguation pages often set Heading and nothing else.
855
- if (data.Heading){
856
- primary = `<p>This looks like a ambiguous term.${escapeHTML(data.Heading)}” may refer to multiple things. Try adding more details (e.g., a location or field).</p>`;
857
- } else {
858
- primary = `<p>Sorry—I couldn't find a direct answer for that. Try rephrasing or asking for “define …”, “weather in …”, or a simple calculation.</p>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
  }
860
- note = 'No direct answer';
861
  }
862
-
863
- return { html: primary, sources, note, raw: data, query: originalQuery };
864
  }
865
 
866
- // Rendering of an answer with small extras for math/units if detected
867
- function enhanceHTML(html){
868
- // If the result seems like math (contains equals or numbers with operators), lightly style
869
- const mathy = /(\d\s*[\+\-\*\/x÷]\s*\d)|(\d+\s*\=\s*\d+)/i.test(stripTags(html));
870
- if (mathy){
871
- html += `<div class="mono" style="margin-top:8px">Tip: For precise calculations, try typing a full expression like “sqrt(144)” or “((3+5)*2)^2”.</div>`;
872
  }
873
- return html;
874
- }
875
-
876
- // Main send handler
877
- async function sendMessage(){
878
- const text = promptEl.value.trim();
879
- if (!text) return;
880
-
881
- // Save to history
882
- history.push({ role: 'user', content: text });
883
-
884
- // Show user message
885
- addMessage('user', `<p>${escapeHTML(text)}</p>`);
886
- promptEl.value = '';
887
- updateTextareaHeight();
888
- renderSuggestions(defaultChips);
889
-
890
- // Prepare typing indicator
891
- const typingRow = addTyping();
892
- sendBtn.disabled = true;
893
- stopBtn.disabled = false;
894
- abortController = new AbortController();
895
-
896
- try{
897
- const data = await fetchDDG(text, abortController.signal);
898
- removeTyping();
899
- const parsed = parseDDG(data, text);
900
- addMessage('bot', enhanceHTML(parsed.html), { badge: 'DuckDuckGo', note: parsed.note, sources: parsed.sources });
901
- history.push({ role: 'assistant', content: parsed.html, sources: parsed.sources });
902
- }catch(err){
903
- removeTyping();
904
- const fallback = `<p>Network error or CORS proxy issue: ${escapeHTML(err.message)}. Please try again.</p>`;
905
- addMessage('bot', fallback, { badge: 'Error' });
906
- history.push({ role: 'assistant', content: fallback });
907
- }finally{
908
- sendBtn.disabled = false;
909
- stopBtn.disabled = true;
910
- abortController = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
911
  }
912
  }
913
 
914
- // UI events
915
- function updateTextareaHeight(){
916
- promptEl.style.height = 'auto';
917
- const newH = Math.min(promptEl.scrollHeight, 200);
918
- promptEl.style.height = Math.max(40, newH) + 'px';
919
- }
920
- promptEl.addEventListener('input', () => {
921
- updateTextareaHeight();
922
- buildPromptChips(promptEl.value);
923
- });
924
- promptEl.addEventListener('keydown', (e) => {
925
- if (e.key === 'Enter' && !e.shiftKey){
926
- e.preventDefault();
927
- if (!sendBtn.disabled) sendMessage();
928
  }
929
- });
930
- sendBtn.addEventListener('click', sendMessage);
931
- stopBtn.addEventListener('click', () => {
932
- if (abortController){
933
- abortController.abort();
934
- removeTyping();
935
- addMessage('bot', `<p>Request canceled.</p>`, { badge: 'Stopped' });
936
- stopBtn.disabled = true;
937
- sendBtn.disabled = false;
938
- abortController = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
939
  }
940
- });
941
-
942
- clearBtn.addEventListener('click', () => {
943
- if (!confirm('Clear the current chat?')) return;
944
- messagesEl.innerHTML = `
945
- <div class="row bot fade-in">
946
- <div class="avatar bot">🤖</div>
947
- <div class="bubble bot">
948
- <div class="meta"><span class="badge">Bot</span><span>DuckDuckGo Instant Answers</span></div>
949
- <div class="answer"><p>Chat cleared. Ask me something new!</p></div>
950
- </div>
951
- </div>`;
952
- history = [];
953
- renderSuggestions(defaultChips);
954
- });
955
-
956
- exportBtn.addEventListener('click', () => {
957
- const payload = {
958
- exportedAt: new Date().toISOString(),
959
- history
960
- };
961
- const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
962
- const url = URL.createObjectURL(blob);
963
- const a = document.createElement('a');
964
- a.href = url;
965
- a.download = 'ddg-chat-export.json';
966
- document.body.appendChild(a);
967
- a.click();
968
- URL.revokeObjectURL(url);
969
- a.remove();
970
- });
971
-
972
- // Initial
973
- renderSuggestions(defaultChips);
974
- updateTextareaHeight();
975
- </script>
976
- </body>
977
-
978
- </html>
 
3
 
4
  <head>
5
  <meta charset="UTF-8" />
6
+ <title>DuckDuckGo AI Chatbot - Enhanced Multi-Source</title>
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <meta name="description" content="An enhanced chatbot that answers using multiple data sources: DuckDuckGo, Wikipedia, and various APIs." />
9
  <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
 
304
  border-radius: 8px;
305
  }
306
 
307
+ .source.multi {
308
+ background: #0d1b3a;
309
+ border-color: #4a90e2;
310
+ }
311
+
312
  .composer {
313
  position: sticky;
314
  bottom: 0;
 
458
  padding: 10px 0 18px;
459
  }
460
 
461
+ .data-source-indicator {
462
+ display: inline-flex;
463
+ align-items: center;
464
+ gap: 4px;
465
+ font-size: 11px;
466
+ padding: 2px 6px;
467
+ border-radius: 6px;
468
+ background: rgba(74, 144, 226, 0.2);
469
+ border: 1px solid rgba(74, 144, 226, 0.4);
470
+ }
471
+
472
+ .data-source-indicator.wiki {
473
+ background: rgba(74, 226, 144, 0.2);
474
+ border-color: rgba(74, 226, 144, 0.4);
475
+ }
476
+
477
+ .data-source-indicator.weather {
478
+ background: rgba(250, 204, 21, 0.2);
479
+ border-color: rgba(250, 204, 21, 0.4);
480
+ }
481
+
482
+ .data-source-indicator.calc {
483
+ background: rgba(168, 85, 247, 0.2);
484
+ border-color: rgba(168, 85, 247, 0.4);
485
+ }
486
+
487
+ .source-badge {
488
+ font-size: 10px;
489
+ padding: 1px 4px;
490
+ border-radius: 3px;
491
+ background: rgba(255, 255, 255, 0.1);
492
+ margin-left: 4px;
493
+ }
494
+
495
  /* Responsive */
496
  @media (max-width: 720px) {
497
  .topbar {
 
557
  padding: 10px;
558
  overflow: auto;
559
  }
560
+
561
+ .search-results {
562
+ background: #0d1431;
563
+ border: 1px solid rgba(255, 255, 255, .1);
564
+ border-radius: 8px;
565
+ padding: 10px;
566
+ margin: 8px 0;
567
+ }
568
+
569
+ .search-result {
570
+ margin-bottom: 8px;
571
+ padding-bottom: 8px;
572
+ border-bottom: 1px solid rgba(255, 255, 255, .05);
573
+ }
574
+
575
+ .search-result:last-child {
576
+ margin-bottom: 0;
577
+ padding-bottom: 0;
578
+ border-bottom: none;
579
+ }
580
+
581
+ .search-result-title {
582
+ font-weight: 600;
583
+ color: #8ad1ff;
584
+ text-decoration: none;
585
+ }
586
+
587
+ .search-result-title:hover {
588
+ text-decoration: underline;
589
+ }
590
+
591
+ .search-result-snippet {
592
+ font-size: 13px;
593
+ color: #c7d5ff;
594
+ margin-top: 2px;
595
+ }
596
  </style>
597
  </head>
598
 
 
603
  <div class="brand">
604
  <div class="logo" aria-hidden="true"></div>
605
  <div class="titles">
606
+ <div class="title">Enhanced Multi-Source AI Chatbot</div>
607
+ <div class="subtitle">Answers from DuckDuckGo, Wikipedia, Weather APIs, and more.</div>
608
  </div>
609
  </div>
610
  <div class="links">
 
616
  </svg>
617
  DuckDuckGo
618
  </a>
619
+ <a class="btn ghost" href="https://en.wikipedia.org/" target="_blank" rel="noopener noreferrer">
620
+ <svg class="icon" viewBox="0 0 24 24" fill="none">
621
+ <path d="M4 4h16v16H4z" stroke="currentColor" stroke-width="2"/><path d="M7 8h10M7 12h10M7 16h6" stroke="currentColor" stroke-width="2"/>
622
+ </svg>
623
+ Wikipedia
624
+ </a>
625
  <button id="exportBtn" class="btn secondary" title="Export chat as JSON">
626
  <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
627
  Export
 
646
  <div class="bubble bot">
647
  <div class="meta">
648
  <span class="badge">Bot</span>
649
+ <span>Multi-Source AI Assistant</span>
650
  </div>
651
  <div class="answer">
652
+ <p>Hi! I'm your enhanced AI assistant with access to multiple data sources including DuckDuckGo Instant Answers, Wikipedia, Weather APIs, and general web search.</p>
653
+ <p class="hint">Try asking about: facts, definitions, weather, time zones, calculations, historical events, scientific information, or general knowledge questions.</p>
 
 
654
  </div>
655
  </div>
656
  </div>
 
675
  </main>
676
 
677
  <div class="footer-note">
678
+ Enhanced with multiple data sources: DuckDuckGo Instant Answers, Wikipedia API, Open-Meteo Weather API, and web search capabilities.
679
  </div>
680
  </div>
681
 
 
701
  let history = [];
702
  let abortController = null;
703
 
704
+ // Enhanced suggestions
705
  const defaultChips = [
706
+ 'weather in Tokyo', 'time in London', 'define artificial intelligence', 'who was Einstein',
707
+ 'sqrt(144)', 'USD to EUR', 'prime factors of 91', 'Python programming history',
708
+ 'what is machine learning', 'how tall is Mount Everest', 'capital of France',
709
+ 'latest news about AI', 'JavaScript vs Python', 'quantum computing explained'
710
  ];
711
 
712
  function renderSuggestions(items=defaultChips){
 
727
  function buildPromptChips(q){
728
  q = q.trim();
729
  const chips = [];
730
+ const hasWeather = /\b(weather|temperature|forecast)\b/i.test(q);
731
  const hasDefine = /\b(define|meaning|definition|what does)\b/i.test(q);
732
  const hasConvert = /\b(to|in|°)\b/i.test(q) || /\b(c|f|cm|inch|kg|lb)\b/.test(q);
733
  const hasTime = /\b(time|datetime|date)\b/i.test(q);
734
  const hasWho = /\b(who is|who's|who’re)\b/i.test(q);
735
  const hasCalc = /[\d\+\-\*\/\^\(\)\.]/.test(q);
736
+ const hasHistory = /\b(history|historical)\b/i.test(q);
737
+ const hasScience = /\b(science|scientific)\b/i.test(q);
738
 
739
  if (q && !hasWeather) chips.push(q.replace(/\b(what is|what's)\b/i,'').trim() + ' weather');
740
  if (q && !hasDefine) chips.push('define ' + q.replace(/\b(define|meaning|definition|what does)\b/i,'').trim());
 
742
  if (q && !hasTime) chips.push('time in ' + q.replace(/\b(what is|what's|time in|date|datetime)\b/ig,'').trim());
743
  if (q && !hasWho) chips.push('who is ' + q.replace(/\b(who is|who's|who’re)\b/ig,'').trim());
744
  if (q && !hasCalc) chips.push(q.replace(/\b(calculate|compute|solve)\b/i,'').trim());
745
+ if (q && !hasHistory) chips.push(q + ' history');
746
+ if (q && !hasScience) chips.push(q + ' science');
747
 
748
  // de-dup
749
  const unique = [...new Set(chips)].filter(Boolean).slice(0,6);
 
767
  metaEl.className = 'meta';
768
  const badge = document.createElement('span');
769
  badge.className = 'badge';
770
+ badge.textContent = role === 'bot' ? (meta.badge || 'AI Assistant') : 'You';
771
  metaEl.appendChild(badge);
772
+
773
+ if (meta.source) {
774
+ const sourceEl = document.createElement('span');
775
+ sourceEl.className = `data-source-indicator ${meta.source.className || ''}`;
776
+ sourceEl.innerHTML = `${meta.source.icon || '📡'} ${meta.source.name}`;
777
+ metaEl.appendChild(sourceEl);
778
+ }
779
+
780
  if (meta.note){
781
  const note = document.createElement('span');
782
  note.textContent = meta.note;
 
796
  sources.className = 'sources';
797
  meta.sources.forEach(src => {
798
  const s = document.createElement('a');
799
+ s.className = `source ${src.multi ? 'multi' : ''}`;
800
  s.href = src.href;
801
  s.target = '_blank';
802
  s.rel = 'noopener noreferrer';
 
826
  metaEl.className = 'meta';
827
  const badge = document.createElement('span');
828
  badge.className = 'badge';
829
+ badge.textContent = 'AI Assistant';
830
  metaEl.appendChild(badge);
831
  const answer = document.createElement('div');
832
  answer.className = 'answer';
 
844
  if (t) t.remove();
845
  }
846
 
847
+ // CORS proxy list
 
 
 
 
 
 
 
 
 
 
 
848
  const CORS_PROXIES = [
 
849
  (target) => `https://cors.isomorphic-git.org/${target}`,
 
850
  (target) => `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(target)}`,
 
851
  (target) => `https://corsproxy.io/?${encodeURIComponent(target)}`
852
  ];
853
 
 
854
  async function fetchWithCorsProxies(targetUrl, signal){
855
  let lastErr = null;
856
  for (const buildProxy of CORS_PROXIES){
 
859
  const res = await fetch(proxyUrl, { signal, headers: { 'Accept': 'application/json' } });
860
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
861
  const contentType = res.headers.get('content-type') || '';
 
862
  if (!contentType.includes('application/json') && !contentType.includes('text/plain')) {
863
  throw new Error('Unexpected content-type: ' + contentType);
864
  }
 
865
  const text = await res.text();
866
  const data = JSON.parse(text);
867
  return data;
868
  }catch(err){
869
  lastErr = err;
 
870
  }
871
  }
872
  throw lastErr || new Error('All CORS proxies failed');
873
  }
874
 
875
+ // Data Source Classes
876
+ class DataSource {
877
+ constructor(name, icon, className) {
878
+ this.name = name;
879
+ this.icon = icon;
880
+ this.className = className;
881
+ }
882
+ async search(query, signal) { throw new Error('Not implemented'); }
883
  }
884
 
885
+ class DuckDuckGoSource extends DataSource {
886
+ constructor() {
887
+ super('DuckDuckGo', '🦆', 'ddg');
888
+ }
889
+
890
+ buildDDGUrl(query){
891
+ const url = new URL('https://api.duckduckgo.com/');
892
+ url.searchParams.set('q', query);
893
+ url.searchParams.set('format', 'json');
894
+ url.searchParams.set('no_html', '1');
895
+ url.searchParams.set('no_redirect', '1');
896
+ url.searchParams.set('skip_disambig', '1');
897
+ return url.toString();
898
  }
899
+
900
+ async search(query, signal) {
901
+ const targetUrl = this.buildDDGUrl(query);
902
+ const data = await fetchWithCorsProxies(targetUrl, signal);
903
+
904
+ const sources = [];
905
+ if (Array.isArray(data.Results)) {
906
+ data.Results.forEach(r => {
907
+ if (r && r.FirstURL && r.Text) sources.push({ href: r.FirstURL, label: new URL(r.FirstURL).hostname.replace('www.','') });
908
+ });
909
+ }
910
+ if (data.AbstractURL) sources.push({ href: data.AbstractURL, label: new URL(data.AbstractURL).hostname.replace('www.','') });
911
+ if (data.Answer && data.AnswerURL) sources.push({ href: data.AnswerURL, label: new URL(data.AnswerURL).hostname.replace('www.','') });
912
+
913
+ let primary = '';
914
+ let note = '';
915
+
916
+ if (data.Answer && data.Answer.trim()){
917
+ primary = data.Answer;
918
+ note = 'Instant Answer';
919
+ } else if (data.Definition && data.Definition.trim()){
920
+ primary = data.Definition;
921
+ note = data.DefinitionSource ? ('Definition • ' + data.DefinitionSource) : 'Definition';
922
+ if (data.DefinitionURL) sources.push({ href: data.DefinitionURL, label: new URL(data.DefinitionURL).hostname.replace('www.','') });
923
+ } else if (data.AbstractText && data.AbstractText.trim()){
924
+ primary = data.AbstractText;
925
+ note = data.Heading ? ('About: ' + data.Heading) : (data.AbstractSource ? data.AbstractSource : 'Abstract');
926
+ } else if (data.RelatedTopics && data.RelatedTopics.length){
927
+ const items = [];
928
+ for (const t of data.RelatedTopics){
929
+ if (t && t.Text && t.FirstURL) {
930
+ items.push(`<li><a href="${t.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(t.Text)}</a></li>`);
931
+ } else if (Array.isArray(t.Topics)) {
932
+ for (const tt of t.Topics){
933
+ if (tt && tt.Text && tt.FirstURL) {
934
+ items.push(`<li><a href="${tt.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(tt.Text)}</a></li>`);
935
+ }
936
  }
937
  }
938
+ if (items.length >= 6) break;
939
+ }
940
+ if (items.length){
941
+ primary = `<p>Here are some related topics I found:</p><ul>${items.join('')}</ul>`;
942
+ note = 'Related topics';
943
  }
 
 
 
 
 
944
  }
945
+
946
+ return {
947
+ found: !!primary,
948
+ html: primary || `<p>No direct answer from DuckDuckGo.</p>`,
949
+ sources,
950
+ note,
951
+ raw: data
952
+ };
953
+ }
954
+ }
955
+
956
+ class WikipediaSource extends DataSource {
957
+ constructor() {
958
+ super('Wikipedia', '📚', 'wiki');
959
  }
960
 
961
+ async search(query, signal) {
962
+ try {
963
+ // Search for pages
964
+ const searchUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`;
965
+ const data = await fetchWithCorsProxies(searchUrl, signal);
966
+
967
+ if (data.extract) {
968
+ const sources = [];
969
+ if (data.content_urls?.desktop?.page) {
970
+ sources.push({
971
+ href: data.content_urls.desktop.page,
972
+ label: 'Wikipedia Article',
973
+ multi: true
974
+ });
975
+ }
976
+
977
+ let html = `<p>${escapeHTML(data.extract)}</p>`;
978
+
979
+ if (data.extract_html) {
980
+ html = data.extract_html;
981
+ }
982
+
983
+ // Add thumbnail if available
984
+ if (data.thumbnail?.source) {
985
+ html = `<img src="${data.thumbnail.source}" alt="${escapeHTML(data.title || 'Image')}" style="max-width: 200px; border-radius: 8px; margin-bottom: 10px;" /><br/>${html}`;
986
+ }
987
+
988
+ return {
989
+ found: true,
990
+ html,
991
+ sources,
992
+ note: data.title ? `Wikipedia: ${data.title}` : 'Wikipedia Article',
993
+ raw: data
994
+ };
995
+ }
996
+
997
+ return { found: false, html: '<p>No Wikipedia article found.</p>', sources: [], note: 'Wikipedia', raw: data };
998
+ } catch (err) {
999
+ return { found: false, html: '<p>Error fetching Wikipedia data.</p>', sources: [], note: 'Wikipedia', raw: null };
1000
  }
 
1001
  }
 
 
1002
  }
1003
 
1004
+ class WeatherSource extends DataSource {
1005
+ constructor() {
1006
+ super('Weather', '🌤️', 'weather');
 
 
 
1007
  }
1008
+
1009
+ async search(query, signal) {
1010
+ const cityMatch = query.match(/\b(?:weather\s+in|weather|temperature\s+in|forecast\s+in)\s+([a-zA-Z\s,]+)/i);
1011
+ if (!cityMatch) {
1012
+ return { found: false, html: '<p>No city specified for weather.</p>', sources: [], note: 'Weather API' };
1013
+ }
1014
+
1015
+ const city = cityMatch[1].trim();
1016
+ try {
1017
+ // Using Open-Meteo API (free, no API key needed)
1018
+ const geocodeUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`;
1019
+ const geoData = await fetchWithCorsProxies(geocodeUrl, signal);
1020
+
1021
+ if (geoData.results && geoData.results.length > 0) {
1022
+ const location = geoData.results[0];
1023
+ const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&current=temperature_2m,weather_code,wind_speed_10m&temperature_unit=celsius&wind_speed_unit=kmh`;
1024
+ const weatherData = await fetchWithCorsProxies(weatherUrl, signal);
1025
+
1026
+ if (weatherData.current) {
1027
+ const temp = weatherData.current.temperature_2m;
1028
+ const weatherCode = weatherData.current.weather_code;
1029
+ const windSpeed = weatherData.current.wind_speed_10m;
1030
+
1031
+ const weatherDescription = this.getWeatherDescription(weatherCode);
1032
+ const sources = [
1033
+ { href: `https://open-meteo.com/`, label: 'Open-Meteo API', multi: true }
1034
+ ];
1035
+
1036
+ const html = `
1037
+ <div class="search-results">
1038
+ <h4>Current Weather in ${escapeHTML(location.name)}, ${escapeHTML(location.country_code)}</h4>
1039
+ <p><strong>Temperature:</strong> ${temp}°C (${Math.round(temp * 9/5 + 32)}°F)</p>
1040
+ <p><strong>Conditions:</strong> ${weatherDescription}</p>
1041
+ <p><strong>Wind Speed:</strong> ${windSpeed} km/h</p>
1042
+ </div>
1043
+ `;
1044
+
1045
+ return {
1046
+ found: true,
1047
+ html,
1048
+ sources,
1049
+ note: `Weather in ${location.name}`,
1050
+ raw: { location, weather: weatherData }
1051
+ };
1052
+ }
1053
+ }
1054
+
1055
+ return { found: false, html: '<p>Could not find weather data for that location.</p>', sources: [], note: 'Weather API' };
1056
+ } catch (err) {
1057
+ return { found: false, html: '<p>Error fetching weather data.</p>', sources: [], note: 'Weather API' };
1058
+ }
1059
+ }
1060
+
1061
+ getWeatherDescription(code) {
1062
+ const weatherCodes = {
1063
+ 0: "Clear sky",
1064
+ 1: "Mainly clear",
1065
+ 2: "Partly cloudy",
1066
+ 3: "Overcast",
1067
+ 45: "Fog",
1068
+ 48: "Depositing rime fog",
1069
+ 51: "Light drizzle",
1070
+ 53: "Moderate drizzle",
1071
+ 55: "Dense drizzle",
1072
+ 56: "Light freezing drizzle",
1073
+ 57: "Dense freezing drizzle",
1074
+ 61: "Slight rain",
1075
+ 63: "Moderate rain",
1076
+ 65: "Heavy rain",
1077
+ 66: "Light freezing rain",
1078
+ 67: "Heavy freezing rain",
1079
+ 71: "Slight snow fall",
1080
+ 73: "Moderate snow fall",
1081
+ 75: "Heavy snow fall",
1082
+ 77: "Snow grains",
1083
+ 80: "Slight rain showers",
1084
+ 81: "Moderate rain showers",
1085
+ 82: "Violent rain showers",
1086
+ 85: "Slight snow showers",
1087
+ 86: "Heavy snow showers",
1088
+ 95: "Slight thunderstorm",
1089
+ 96: "Thunderstorm with slight hail",
1090
+ 99: "Thunderstorm with heavy hail"
1091
+ };
1092
+ return weatherCodes[code] || "Unknown";
1093
  }
1094
  }
1095
 
1096
+ class CalculatorSource extends DataSource {
1097
+ constructor() {
1098
+ super('Calculator', '🧮', 'calc');
 
 
 
 
 
 
 
 
 
 
 
1099
  }
1100
+
1101
+ async search(query, signal) {
1102
+ // Simple math expression evaluation
1103
+ const mathMatch = query.match(/([\d\+\-\*\/\^\(\)\.\s]+)/);
1104
+ if (!mathMatch) {
1105
+ return { found: false, html: '<p>No mathematical expression found.</p>', sources: [], note: 'Calculator' };
1106
+ }
1107
+
1108
+ try {
1109
+ // Simple and safe evaluation for basic math
1110
+ const expression = mathMatch[1].replace(/\s/g, '');
1111
+
1112
+ // Basic validation to prevent code injection
1113
+ if (!/^[\d\+\-\*\/\^\(\)\.]+$/.test(expression)) {
1114
+ return { found: false, html: '<p>Invalid mathematical expression.</p>', sources: [], note: 'Calculator' };
1115
+ }
1116
+
1117
+ // Very basic math evaluation (you might want to use a proper math library)
1118
+ const result = this.safeEval(expression);
1119
+
1120
+ if (result !== null && result !== undefined) {
1121
+ const html = `
1122
+ <div class="search-results">
1123
+ <h4>Calculation Result</h4>
1124
+ <p><strong>Expression:</strong> <code>${escapeHTML(expression)}</code></p>
1125
+ <p><strong>Result:</strong> <span style="font-size: 18px; color: var(--success);">${result}</span></p>
1126
+ </div>
1127
+ `;
1128
+
1129
+ return {
1130
+ found: true,
1131
+ html,
1132
+ sources: [],
1133
+ note: `Math Calculation`,
1134
+ raw: { expression, result }
1135
+ };
1136
+ }
1137
+
1138
+ return { found: false, html: '<p>Could not evaluate the expression.</p>', sources: [], note: 'Calculator' };
1139
+ } catch (err) {
1140
+ return { found: false, html: '<p>Error evaluating mathematical expression.</p>', sources: [], note: 'Calculator' };
1141
+ }
1142
  }
1143
+
1144
+ safeEval(expression) {
1145
+ try {
1146
+ // This is a very basic evaluator. For production,