AiCoderv2 commited on
Commit
c80854f
·
verified ·
1 Parent(s): 0fe09d3

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +294 -817
index.html CHANGED
@@ -1,605 +1,188 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8" />
5
  <title>Enhanced Multi-Source AI Chatbot</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1" />
7
- <meta name="description"
8
- content="A responsive multi-source chatbot that answers using DuckDuckGo, Wikipedia, Weather APIs, and built-in offline knowledge/fallbacks." />
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">
12
  <style>
13
  :root {
14
- --bg: #0b1020;
15
- --bg2: #0f1630;
16
- --panel: #0f1732;
17
- --panel-2: #111a3b;
18
- --text: #e7ecff;
19
- --muted: #a3b1d9;
20
- --accent: #6ea8fe;
21
- --accent-2: #8ad1ff;
22
- --success: #4ade80;
23
- --warning: #facc15;
24
- --danger: #f87171;
25
- --bubble-user: #1a254f;
26
- --bubble-bot: #141e3d;
27
- --shadow: 0 10px 30px rgba(0, 0, 0, .35);
28
- --radius: 14px;
29
  }
30
-
31
  * { box-sizing: border-box }
32
-
33
- html, body {
34
- margin: 0;
35
- padding: 0;
36
- height: 100%;
37
- font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
38
- color: var(--text);
39
- background:
40
- radial-gradient(1200px 800px at 10% -20%, #20306a 0%, transparent 60%),
41
  radial-gradient(900px 700px at 110% 10%, #1b3c7a 0%, transparent 60%),
42
  radial-gradient(700px 900px at 50% 120%, #1a365d 0%, transparent 60%),
43
- linear-gradient(180deg, var(--bg), var(--bg2));
44
- background-attachment: fixed;
45
  }
46
-
47
- a { color: var(--accent) }
48
-
49
- .app {
50
- display: flex;
51
- flex-direction: column;
52
- min-height: 100%;
53
- width: 100%;
54
- max-width: 1100px;
55
- margin: 0 auto;
56
- }
57
-
58
  header {
59
- position: sticky;
60
- top: 0;
61
- z-index: 10;
62
- backdrop-filter: saturate(180%) blur(8px);
63
- background: linear-gradient(180deg, rgba(10, 16, 34, .8), rgba(10, 16, 34, .35));
64
- border-bottom: 1px solid rgba(255, 255, 255, .06);
65
- }
66
-
67
- .topbar {
68
- display: flex;
69
- align-items: center;
70
- justify-content: space-between;
71
- padding: 14px 18px;
72
- gap: 12px;
73
  }
74
-
75
- .brand {
76
- display: flex;
77
- align-items: center;
78
- gap: 12px;
79
- min-width: 0;
80
- }
81
-
82
  .logo {
83
- width: 36px;
84
- height: 36px;
85
- border-radius: 10px;
86
  background: conic-gradient(from 210deg, #6ea8fe, #8ad1ff, #6ea8fe);
87
- box-shadow: 0 6px 16px rgba(110, 168, 254, .35), inset 0 0 18px rgba(255, 255, 255, .2);
88
- position: relative;
89
  }
90
-
91
  .logo::after {
92
- content: "";
93
- position: absolute;
94
- inset: 3px;
95
- border-radius: 8px;
96
- background: radial-gradient(120px 80px at 30% 20%, rgba(255, 255, 255, .5), transparent 40%),
97
- linear-gradient(180deg, rgba(255, 255, 255, .12), rgba(0, 0, 0, .18));
98
- }
99
-
100
- .titles {
101
- min-width: 0;
102
- }
103
-
104
- .title {
105
- font-weight: 800;
106
- letter-spacing: .2px;
107
- font-size: 18px;
108
- white-space: nowrap;
109
- overflow: hidden;
110
- text-overflow: ellipsis;
111
- }
112
-
113
- .subtitle {
114
- font-size: 12px;
115
- color: var(--muted);
116
- }
117
-
118
- .links {
119
- display: flex;
120
- gap: 10px;
121
- align-items: center;
122
- flex-wrap: wrap;
123
  }
124
-
 
 
125
  .btn {
126
- appearance: none;
127
- border: none;
128
- cursor: pointer;
129
- font: inherit;
130
- color: inherit;
131
- padding: 10px 14px;
132
- border-radius: 10px;
133
- background: #1a254f;
134
- color: #dfe8ff;
135
- display: inline-flex;
136
- align-items: center;
137
- gap: 8px;
138
- border: 1px solid rgba(255, 255, 255, .08);
139
- transition: transform .08s ease, background .2s ease, border-color .2s ease, opacity .2s ease;
140
- }
141
-
142
- .btn:hover {
143
- transform: translateY(-1px);
144
- border-color: rgba(255, 255, 255, .18)
145
- }
146
-
147
- .btn.secondary {
148
- background: #0f1732;
149
- }
150
-
151
- .btn.ghost {
152
- background: transparent;
153
- border-color: rgba(255, 255, 255, .1)
154
- }
155
-
156
- .btn:disabled {
157
- opacity: .6;
158
- cursor: not-allowed;
159
- transform: none
160
- }
161
-
162
- .icon {
163
- width: 18px;
164
- height: 18px;
165
- display: inline-block;
166
- }
167
-
168
- main {
169
- flex: 1;
170
- display: flex;
171
- flex-direction: column;
172
- padding: 12px;
173
- }
174
-
175
- .chat {
176
- flex: 1;
177
- overflow: auto;
178
- padding: 12px;
179
- padding-bottom: 24px;
180
- scroll-behavior: smooth;
181
- }
182
-
183
- .messages {
184
- display: flex;
185
- flex-direction: column;
186
- gap: 12px;
187
- }
188
-
189
- .row {
190
- display: flex;
191
- gap: 10px;
192
- align-items: flex-end;
193
- }
194
-
195
- .row.user {
196
- justify-content: flex-end;
197
- }
198
-
199
- .row.bot {
200
- justify-content: flex-start;
201
- }
202
-
203
  .avatar {
204
- width: 32px;
205
- height: 32px;
206
- border-radius: 50%;
207
- background: #172046;
208
- display: flex;
209
- align-items: center;
210
- justify-content: center;
211
- font-size: 14px;
212
- font-weight: 700;
213
- border: 1px solid rgba(255, 255, 255, .08);
214
- box-shadow: var(--shadow);
215
- flex-shrink: 0;
216
- }
217
-
218
- .avatar.bot {
219
- background: radial-gradient(100px 80px at 20% 10%, #2a3d7a, #192555);
220
  }
221
-
222
- .avatar.user {
223
- background: radial-gradient(120px 90px at 30% 10%, #1a2c5f, #0e1736);
224
- }
225
-
226
  .bubble {
227
- max-width: min(800px, 86vw);
228
- padding: 12px 14px;
229
- border-radius: var(--radius);
230
- border: 1px solid rgba(255, 255, 255, .08);
231
- box-shadow: var(--shadow);
232
- line-height: 1.55;
233
- position: relative;
234
- word-wrap: break-word;
235
- overflow-wrap: anywhere;
236
- }
237
-
238
- .bubble.user {
239
- background: linear-gradient(180deg, #152152, #101a3e);
240
- border-top-right-radius: 6px;
241
- }
242
-
243
- .bubble.bot {
244
- background: linear-gradient(180deg, #111a3b, #0c1431);
245
- border-bottom-left-radius: 6px;
246
- }
247
-
248
- .bubble .meta {
249
- font-size: 12px;
250
- color: var(--muted);
251
- margin-bottom: 6px;
252
- display: flex;
253
- gap: 8px;
254
- align-items: center;
255
- flex-wrap: wrap;
256
- }
257
-
258
- .badge {
259
- padding: 2px 8px;
260
- border-radius: 999px;
261
- background: #0c1330;
262
- border: 1px solid rgba(255, 255, 255, .08);
263
- font-size: 11px;
264
- color: #c7d5ff;
265
- }
266
-
267
- .answer {
268
- font-size: 15px;
269
- color: #e9efff;
270
- }
271
-
272
- .answer p { margin: 0 0 10px 0; }
273
-
274
- .answer ul, .answer ol {
275
- margin: 6px 0 10px 18px;
276
- }
277
-
278
- .answer a {
279
- color: var(--accent-2);
280
- text-decoration: underline;
281
- }
282
-
283
- .sources {
284
- display: flex;
285
- gap: 8px;
286
- flex-wrap: wrap;
287
- margin-top: 8px;
288
- }
289
-
290
  .source {
291
- font-size: 12px;
292
- color: #c7d5ff;
293
- background: #0c1330;
294
- border: 1px solid rgba(255, 255, 255, .08);
295
- padding: 4px 8px;
296
- border-radius: 8px;
297
  }
298
-
299
- .source.multi {
300
- background: #0d1b3a;
301
- border-color: #4a90e2;
302
- }
303
-
304
  .composer {
305
- position: sticky;
306
- bottom: 0;
307
- z-index: 9;
308
- display: flex;
309
- gap: 10px;
310
- align-items: flex-end;
311
- padding: 12px;
312
- background: linear-gradient(180deg, rgba(10, 16, 34, .0), rgba(10, 16, 34, .85));
313
- backdrop-filter: blur(8px);
314
- border-top: 1px solid rgba(255, 255, 255, .06);
315
  }
316
-
317
  .input-wrap {
318
- flex: 1;
319
- display: flex;
320
- gap: 10px;
321
- align-items: flex-end;
322
- background: linear-gradient(180deg, #0e1633, #0b122b);
323
- border: 1px solid rgba(255, 255, 255, .08);
324
- border-radius: 14px;
325
- padding: 8px;
326
- box-shadow: var(--shadow);
327
  }
328
-
329
  textarea {
330
- flex: 1;
331
- resize: none;
332
- border: none;
333
- outline: none;
334
- background: transparent;
335
- color: var(--text);
336
- font: inherit;
337
- line-height: 1.4;
338
- max-height: 200px;
339
- min-height: 40px;
340
- padding: 8px 10px;
341
- }
342
-
343
- .actions {
344
- display: flex;
345
- gap: 8px;
346
- align-items: center;
347
  }
348
-
349
  .small-btn {
350
- width: 38px;
351
- height: 38px;
352
- border-radius: 10px;
353
- border: 1px solid rgba(255, 255, 255, .1);
354
- background: #0e1633;
355
- display: flex;
356
- align-items: center;
357
- justify-content: center;
358
- cursor: pointer;
359
  transition: transform .08s ease, border-color .2s ease, background .2s ease;
360
  }
361
-
362
- .small-btn:hover {
363
- transform: translateY(-1px);
364
- border-color: rgba(255, 255, 255, .2)
365
- }
366
-
367
- .small-btn:disabled {
368
- opacity: .6;
369
- cursor: not-allowed;
370
- transform: none
371
- }
372
-
373
  .suggestions {
374
- position: absolute;
375
- left: 12px;
376
- right: 12px;
377
- bottom: 64px;
378
- display: flex;
379
- flex-wrap: wrap;
380
- gap: 8px;
381
- pointer-events: none;
382
  }
383
-
384
  .chip {
385
- pointer-events: auto;
386
- background: #0d1431;
387
- border: 1px solid rgba(255, 255, 255, .1);
388
- color: #d8e4ff;
389
- padding: 6px 10px;
390
- border-radius: 999px;
391
- font-size: 12px;
392
- cursor: pointer;
393
  transition: transform .08s ease, background .2s ease, border-color .2s ease;
394
  }
395
-
396
- .chip:hover {
397
- transform: translateY(-1px);
398
- background: #121c44;
399
- border-color: rgba(255, 255, 255, .2)
400
- }
401
-
402
- .typing {
403
- display: inline-flex;
404
- align-items: center;
405
- gap: 6px;
406
- }
407
-
408
- .dot {
409
- width: 6px;
410
- height: 6px;
411
- background: #c7d5ff;
412
- border-radius: 50%;
413
- opacity: .8;
414
- animation: blink 1.4s infinite ease-in-out;
415
- }
416
-
417
  .dot:nth-child(2) { animation-delay: .2s }
418
  .dot:nth-child(3) { animation-delay: .4s }
419
-
420
- @keyframes blink {
421
- 0%, 80%, 100% { transform: translateY(0); opacity: .5 }
422
- 40% { transform: translateY(-3px); opacity: 1 }
423
- }
424
-
425
  .hint { font-size: 12px; color: var(--muted); padding: 4px 2px 0 2px; }
426
-
427
- .footer-note {
428
- text-align: center;
429
- font-size: 12px;
430
- color: var(--muted);
431
- padding: 10px 0 18px;
432
- }
433
-
434
  .data-source-indicator {
435
- display: inline-flex;
436
- align-items: center;
437
- gap: 4px;
438
- font-size: 11px;
439
- padding: 2px 6px;
440
- border-radius: 6px;
441
- background: rgba(74, 144, 226, 0.2);
442
- border: 1px solid rgba(74, 144, 226, 0.4);
443
- }
444
-
445
- .data-source-indicator.wiki {
446
- background: rgba(74, 226, 144, 0.2);
447
- border-color: rgba(74, 226, 144, 0.4);
448
- }
449
-
450
- .data-source-indicator.weather {
451
- background: rgba(250, 204, 21, 0.2);
452
- border-color: rgba(250, 204, 21, 0.4);
453
- }
454
-
455
- .data-source-indicator.calc {
456
- background: rgba(168, 85, 247, 0.2);
457
- border-color: rgba(168, 85, 247, 0.4);
458
- }
459
-
460
- .data-source-indicator.offline {
461
- background: rgba(248, 113, 113, 0.2);
462
- border-color: rgba(248, 113, 113, 0.4);
463
- }
464
-
465
- .source-badge {
466
- font-size: 10px;
467
- padding: 1px 4px;
468
- border-radius: 3px;
469
- background: rgba(255, 255, 255, 0.1);
470
- margin-left: 4px;
471
- }
472
-
473
  .mono {
474
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
475
- font-size: 12px;
476
- background: #0b1434;
477
- border: 1px solid rgba(255, 255, 255, .08);
478
- border-radius: 8px;
479
- padding: 10px;
480
- overflow: auto;
481
- }
482
-
483
- .search-results {
484
- background: #0d1431;
485
- border: 1px solid rgba(255, 255, 255, .1);
486
- border-radius: 8px;
487
- padding: 10px;
488
- margin: 8px 0;
489
- }
490
-
491
- .search-result {
492
- margin-bottom: 8px;
493
- padding-bottom: 8px;
494
- border-bottom: 1px solid rgba(255, 255, 255, .05);
495
- }
496
-
497
- .search-result:last-child {
498
- margin-bottom: 0;
499
- padding-bottom: 0;
500
- border-bottom: none;
501
- }
502
-
503
- .search-result-title {
504
- font-weight: 600;
505
- color: #8ad1ff;
506
- text-decoration: none;
507
- }
508
-
509
- .search-result-title:hover {
510
- text-decoration: underline;
511
- }
512
-
513
- .search-result-snippet {
514
- font-size: 13px;
515
- color: #c7d5ff;
516
- margin-top: 2px;
517
- }
518
-
519
  .status {
520
- position: fixed;
521
- bottom: 10px;
522
- right: 12px;
523
- background: rgba(12, 19, 48, 0.8);
524
- border: 1px solid rgba(255, 255, 255, .1);
525
- padding: 6px 10px;
526
- border-radius: 999px;
527
- font-size: 12px;
528
- color: var(--muted);
529
- display: flex;
530
- align-items: center;
531
- gap: 8px;
532
- backdrop-filter: blur(6px);
533
- }
534
-
535
- .status-dot {
536
- width: 8px;
537
- height: 8px;
538
- border-radius: 50%;
539
- background: var(--warning);
540
- box-shadow: 0 0 8px var(--warning);
541
  }
542
-
543
- .status-dot.ok { background: var(--success); box-shadow: 0 0 8px var(--success); }
544
- .status-dot.err { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
545
-
546
- /* Responsive */
547
  @media (max-width: 720px) {
548
  .topbar { padding: 12px }
549
- .titles .title { font-size: 17px }
550
  .links { display: none }
551
  .bubble { max-width: min(760px, 92vw) }
552
  .suggestions { bottom: 60px }
553
  }
554
-
555
- /* Scrollbar */
556
  .chat::-webkit-scrollbar { width: 10px }
557
- .chat::-webkit-scrollbar-thumb {
558
- background: #16214a;
559
- border-radius: 10px;
560
- border: 2px solid transparent;
561
- background-clip: padding-box;
562
- }
563
  .chat::-webkit-scrollbar-track { background: transparent }
564
-
565
- /* Simple fade-in for messages */
566
- .fade-in {
567
- animation: fadeIn .22s ease-out;
568
- }
569
-
570
- @keyframes fadeIn {
571
- from { opacity: 0; transform: translateY(6px) }
572
- to { opacity: 1; transform: translateY(0) }
573
- }
574
  </style>
575
  </head>
576
-
577
  <body>
578
  <div class="app">
579
  <header>
580
  <div class="topbar">
581
  <div class="brand">
582
- <div class="logo" aria-hidden="true"></div>
583
- <div class="titles">
584
  <div class="title">Enhanced Multi-Source AI Chatbot</div>
585
  <div class="subtitle">DuckDuckGo, Wikipedia, Weather, Calculator + Offline Fallbacks</div>
586
  </div>
587
  </div>
588
  <div class="links">
589
- <a class="btn ghost" href="https://duckduckgo.com/" target="_blank" rel="noopener noreferrer">
590
- <svg class="icon" viewBox="0 0 24 24" fill="none">
591
- <path d="M12 2a10 10 0 1 0 .001 20.001A10 10 0 0 0 12 2Zm0 0c2.5 0 4.5 5 4.5 5s-2 5-4.5 5-4.5-5-4.5-5 2-5 4.5-5Zm0 10c4.5 0 6 5 6 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
592
- </svg>
593
  DuckDuckGo
594
  </a>
595
- <a class="btn ghost" href="https://en.wikipedia.org/" target="_blank" rel="noopener noreferrer">
596
- <svg class="icon" viewBox="0 0 24 24" fill="none">
597
- <path d="M4 4h16v16H4z" stroke="currentColor" stroke-width="2" />
598
- <path d="M7 8h10M7 12h10M7 16h6" stroke="currentColor" stroke-width="2" />
599
- </svg>
600
  Wikipedia
601
  </a>
602
- <button id="exportBtn" class="btn secondary" title="Export chat as JSON">
603
  <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>
604
  Export
605
  </button>
@@ -607,23 +190,17 @@
607
  <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M3 6h18M8 6v12m8-12v12M5 6l1 14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2L19 6M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
608
  Clear
609
  </button>
610
- <a class="btn" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener noreferrer" title="Built with anycoder">
611
- Built with anycoder
612
- </a>
613
  </div>
614
  </div>
615
  </header>
616
-
617
  <main>
618
  <div class="chat" id="chat">
619
  <div class="messages" id="messages">
620
  <div class="row bot fade-in">
621
  <div class="avatar bot">🤖</div>
622
  <div class="bubble bot">
623
- <div class="meta">
624
- <span class="badge">Bot</span>
625
- <span>Multi-Source AI Assistant</span>
626
- </div>
627
  <div class="answer">
628
  <p>Hi! I'm your enhanced AI assistant with multiple data sources including DuckDuckGo Instant Answers, Wikipedia, Weather APIs, Calculator, and robust offline fallbacks.</p>
629
  <p class="hint">Try asking about: facts, definitions, weather, time zones, calculations, historical events, or general knowledge.</p>
@@ -632,9 +209,7 @@
632
  </div>
633
  </div>
634
  </div>
635
-
636
  <div class="suggestions" id="suggestions"></div>
637
-
638
  <div class="composer">
639
  <div class="input-wrap">
640
  <textarea id="prompt" rows="1" placeholder="Ask me anything... (Shift+Enter = newline)"></textarea>
@@ -649,92 +224,48 @@
649
  </div>
650
  </div>
651
  </main>
652
-
653
- <div class="footer-note">
654
- Enhanced with multiple data sources: DuckDuckGo, Wikipedia, Open-Meteo Weather API, Calculator, and offline knowledge.
655
- </div>
656
  </div>
657
-
658
  <div class="status" id="status">
659
  <span class="status-dot" id="statusDot"></span>
660
  <span id="statusText">Checking network...</span>
661
  </div>
662
-
663
  <script>
664
  // Utilities
665
- const $ = (sel, el=document) => el.querySelector(sel);
666
- const $$ = (sel, el=document) => [...el.querySelectorAll(sel)];
667
- const escapeHTML = (s) => s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
668
- const stripTags = (s) => s.replace(/<[^>]*>/g, '');
669
- const truncate = (s, n=220) => s.length > n ? s.slice(0, n-1) + '…' : s;
670
  const nowTime = () => new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
671
-
672
  function updateTextareaHeight() {
673
- const el = $('#prompt');
674
- el.style.height = 'auto';
675
- el.style.height = Math.min(el.scrollHeight, 200) + 'px';
676
  }
677
-
678
  // DOM elements
679
- const chatEl = $('#chat');
680
- const messagesEl = $('#messages');
681
- const promptEl = $('#prompt');
682
- const sendBtn = $('#sendBtn');
683
- const stopBtn = $('#stopBtn');
684
- const suggestionsEl = $('#suggestions');
685
- const clearBtn = $('#clearBtn');
686
- const exportBtn = $('#exportBtn');
687
- const statusDot = $('#statusDot');
688
- const statusText = $('#statusText');
689
-
690
  // State
691
- let history = [];
692
- let abortController = null;
693
- let lastQuery = '';
694
- let offlineMode = false;
695
-
696
  // Network indicator
697
  function updateNetworkStatus() {
698
- if (navigator.onLine) {
699
- statusDot.className = 'status-dot ok';
700
- statusText.textContent = 'Online';
701
- offlineMode = false;
702
- } else {
703
- statusDot.className = 'status-dot err';
704
- statusText.textContent = 'Offline';
705
- offlineMode = true;
706
- }
707
  }
708
- window.addEventListener('online', updateNetworkStatus);
709
- window.addEventListener('offline', updateNetworkStatus);
710
- updateNetworkStatus();
711
-
712
  // Suggestions
713
- const defaultChips = [
714
- 'weather in Tokyo', 'time in London', 'define artificial intelligence', 'who was Einstein',
715
- 'sqrt(144)', 'USD to EUR', 'prime factors of 91', 'Python programming history',
716
- 'what is machine learning', 'how tall is Mount Everest', 'capital of France',
717
- 'latest news about AI', 'JavaScript vs Python', 'quantum computing explained'
718
- ];
719
-
720
  function renderSuggestions(items=defaultChips){
721
- suggestionsEl.innerHTML = '';
722
- items.forEach(text => {
723
- const chip = document.createElement('button');
724
- chip.className = 'chip';
725
- chip.textContent = text;
726
- chip.onclick = () => {
727
- promptEl.value = text;
728
- updateTextareaHeight();
729
- promptEl.focus();
730
- };
731
  suggestionsEl.appendChild(chip);
732
  });
733
  }
734
-
735
  function buildPromptChips(q){
736
- q = q.trim();
737
- const chips = [];
738
  const hasWeather = /\b(weather|temperature|forecast)\b/i.test(q);
739
  const hasDefine = /\b(define|meaning|definition|what does)\b/i.test(q);
740
  const hasConvert = /\b(to|in|°)\b/i.test(q) || /\b(c|f|cm|inch|kg|lb)\b/.test(q);
@@ -743,8 +274,8 @@
743
  const hasCalc = /[\d\+\-\*\/\^\(\)\.]/.test(q);
744
  const hasHistory = /\b(history|historical)\b/i.test(q);
745
  const hasScience = /\b(science|scientific)\b/i.test(q);
746
-
747
- if (q && !hasWeather) chips.push(q.replace(/\b(what is|what's)\b/i,'').trim() + ' weather');
748
  if (q && !hasDefine) chips.push('define ' + q.replace(/\b(define|meaning|definition|what does)\b/i,'').trim());
749
  if (q && !hasConvert && /(?:\d|\b)(°\s?[cf]|celsius|fahrenheit|cm|inch|kg|lb)\b/i.test(q)) chips.push(q + ' to fahrenheit');
750
  if (q && !hasTime) chips.push('time in ' + q.replace(/\b(what is|what's|time in|date|datetime)\b/ig,'').trim());
@@ -752,112 +283,58 @@
752
  if (q && !hasCalc) chips.push(q.replace(/\b(calculate|compute|solve)\b/i,'').trim());
753
  if (q && !hasHistory) chips.push(q + ' history');
754
  if (q && !hasScience) chips.push(q + ' science');
755
-
756
  const unique = [...new Set(chips)].filter(Boolean).slice(0,6);
757
- if (unique.length === 0) renderSuggestions(defaultChips);
758
- else renderSuggestions(unique);
759
  }
760
-
761
  // Message helpers
762
  function addMessage(role, contentHTML, meta = {}){
763
- const row = document.createElement('div');
764
- row.className = `row ${role} fade-in`;
765
-
766
- const avatar = document.createElement('div');
767
- avatar.className = `avatar ${role}`;
768
- avatar.textContent = role === 'bot' ? '🤖' : '🙂';
769
-
770
- const bubble = document.createElement('div');
771
- bubble.className = `bubble ${role}`;
772
-
773
- const metaEl = document.createElement('div');
774
- metaEl.className = 'meta';
775
- const badge = document.createElement('span');
776
- badge.className = 'badge';
777
- badge.textContent = role === 'bot' ? (meta.badge || 'AI Assistant') : 'You';
778
  metaEl.appendChild(badge);
779
-
780
  if (meta.source) {
781
- const sourceEl = document.createElement('span');
782
- sourceEl.className = `data-source-indicator ${meta.source.className || ''}`;
783
  sourceEl.innerHTML = `${meta.source.icon || '📡'} ${meta.source.name}`;
784
  metaEl.appendChild(sourceEl);
785
  }
786
-
787
- if (meta.note){
788
- const note = document.createElement('span');
789
- note.textContent = meta.note;
790
- note.style.color = 'var(--muted)';
791
- metaEl.appendChild(note);
792
- }
793
-
794
- const answer = document.createElement('div');
795
- answer.className = 'answer';
796
- answer.innerHTML = contentHTML;
797
-
798
- bubble.appendChild(metaEl);
799
- bubble.appendChild(answer);
800
-
801
  if (meta.sources && meta.sources.length){
802
- const sources = document.createElement('div');
803
- sources.className = 'sources';
804
  meta.sources.forEach(src => {
805
- const s = document.createElement('a');
806
- s.className = `source ${src.multi ? 'multi' : ''}`;
807
- s.href = src.href;
808
- s.target = '_blank';
809
- s.rel = 'noopener noreferrer';
810
- s.textContent = src.label || src.href;
811
- sources.appendChild(s);
812
  });
813
  bubble.appendChild(sources);
814
  }
815
-
816
- row.appendChild(avatar);
817
- row.appendChild(bubble);
818
- messagesEl.appendChild(row);
819
- chatEl.scrollTop = chatEl.scrollHeight;
820
- return bubble;
821
  }
822
-
823
  function addTyping(){
824
- const row = document.createElement('div');
825
- row.className = 'row bot fade-in';
826
- row.dataset.typing = '1';
827
- const avatar = document.createElement('div');
828
- avatar.className = 'avatar bot';
829
- avatar.textContent = '🤖';
830
- const bubble = document.createElement('div');
831
- bubble.className = 'bubble bot';
832
- const metaEl = document.createElement('div');
833
- metaEl.className = 'meta';
834
- const badge = document.createElement('span');
835
- badge.className = 'badge';
836
- badge.textContent = 'AI Assistant';
837
  metaEl.appendChild(badge);
838
- const answer = document.createElement('div');
839
- answer.className = 'answer';
840
  answer.innerHTML = `<span class="typing"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span>`;
841
- bubble.appendChild(metaEl);
842
- bubble.appendChild(answer);
843
- row.appendChild(avatar);
844
- row.appendChild(bubble);
845
- messagesEl.appendChild(row);
846
- chatEl.scrollTop = chatEl.scrollHeight;
847
- return row;
848
- }
849
- function removeTyping(){
850
- const t = $('[data-typing="1"]', messagesEl);
851
- if (t) t.remove();
852
  }
853
-
 
854
  // CORS proxy list
855
  const CORS_PROXIES = [
856
- (target) => `https://cors.isomorphic-git.org/${target}`,
857
- (target) => `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(target)}`,
858
- (target) => `https://corsproxy.io/?${encodeURIComponent(target)}`
859
  ];
860
-
861
  async function fetchWithCorsProxies(targetUrl, signal){
862
  let lastErr = null;
863
  for (const buildProxy of CORS_PROXIES){
@@ -868,179 +345,179 @@
868
  const contentType = res.headers.get('content-type') || '';
869
  const text = await res.text();
870
  let data = text;
871
- if (contentType.includes('application/json')) {
872
- try { data = JSON.parse(text); } catch { /* keep text */ }
873
- }
874
  return data;
875
- }catch(err){
876
- lastErr = err;
877
- }
878
  }
879
  throw lastErr || new Error('All CORS proxies failed');
880
  }
881
-
882
  // Data Source Classes
883
  class DataSource {
884
- constructor(name, icon, className) {
885
- this.name = name;
886
- this.icon = icon;
887
- this.className = className;
888
- }
889
  async search(query, signal) { throw new Error('Not implemented'); }
890
  getPriority() { return 50; }
891
  canHandle() { return false; }
892
  }
893
-
894
  class OfflineSource extends DataSource {
895
- constructor() {
896
- super('Offline', '💾', 'offline');
897
- }
898
-
899
  canHandle() { return offlineMode; }
900
-
901
- getPriority() { return 5; } // lowest priority
902
-
903
  async search(query, signal) {
904
- // Use built-in knowledge base as fallback
905
  const kb = LocalKnowledgeBase;
906
  const found = kb.search(query);
907
  if (found) {
908
- return {
909
- found: true,
910
- html: found.html,
911
- sources: found.sources || [],
912
- note: found.note || 'Offline knowledge',
913
- raw: { query }
914
- };
915
  }
916
- return {
917
- found: false,
918
- html: `<p>I'm currently offline and don't have information about that. Please try a rephrase or check your network connection.</p>`,
919
- sources: [],
920
- note: 'Offline',
921
- raw: { query }
922
- };
923
  }
924
  }
925
-
926
  class DuckDuckGoSource extends DataSource {
927
- constructor() {
928
- super('DuckDuckGo', '🦆', 'ddg');
929
- }
930
-
931
- buildDDGUrl(query){
932
- const url = new URL('https://api.duckduckgo.com/');
933
- url.searchParams.set('q', query);
934
- url.searchParams.set('format', 'json');
935
- url.searchParams.set('no_html', '1');
936
- url.searchParams.set('no_redirect', '1');
937
- url.searchParams.set('skip_disambig', '1');
938
- return url.toString();
939
- }
940
-
941
- canHandle() {
942
- return true; // try for general queries
943
- }
944
-
945
  getPriority() { return 40; }
946
-
947
  async search(query, signal) {
948
  const targetUrl = this.buildDDGUrl(query);
949
  try {
950
  const data = await fetchWithCorsProxies(targetUrl, signal);
951
  const sources = [];
952
  if (Array.isArray(data.Results)) {
953
- data.Results.forEach(r => {
954
- if (r && r.FirstURL && r.Text) sources.push({ href: r.FirstURL, label: new URL(r.FirstURL).hostname.replace('www.','') });
955
- });
956
  }
957
  if (data.AbstractURL) sources.push({ href: data.AbstractURL, label: new URL(data.AbstractURL).hostname.replace('www.','') });
958
  if (data.Answer && data.AnswerURL) sources.push({ href: data.AnswerURL, label: new URL(data.AnswerURL).hostname.replace('www.','') });
959
-
960
- let primary = '';
961
- let note = '';
962
-
963
- if (data.Answer && data.Answer.trim()){
964
- primary = data.Answer;
965
- note = 'Instant Answer';
966
- } else if (data.Definition && data.Definition.trim()){
967
- primary = data.Definition;
968
- note = data.DefinitionSource ? ('Definition • ' + data.DefinitionSource) : 'Definition';
969
- if (data.DefinitionURL) sources.push({ href: data.DefinitionURL, label: new URL(data.DefinitionURL).hostname.replace('www.','') });
970
- } else if (data.AbstractText && data.AbstractText.trim()){
971
- primary = data.AbstractText;
972
- note = data.Heading ? ('About: ' + data.Heading) : (data.AbstractSource ? data.AbstractSource : 'Abstract');
973
- } else if (data.RelatedTopics && data.RelatedTopics.length){
974
  const items = [];
975
  for (const t of data.RelatedTopics){
976
- if (t && t.Text && t.FirstURL) {
977
- items.push(`<li><a href="${t.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(t.Text)}</a></li>`);
978
- } else if (Array.isArray(t.Topics)) {
979
- for (const tt of t.Topics){
980
- if (tt && tt.Text && tt.FirstURL) {
981
- items.push(`<li><a href="${tt.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(tt.Text)}</a></li>`);
982
- }
983
- }
984
  }
985
  if (items.length >= 6) break;
986
  }
987
- if (items.length){
988
- primary = `<p>Here are some related topics I found:</p><ul>${items.join('')}</ul>`;
989
- note = 'Related topics';
990
- }
991
- }
992
-
993
- if (!primary) {
994
- return { found: false, html: '<p>No direct answer from DuckDuckGo.</p>', sources, note: 'DuckDuckGo', raw: data };
995
  }
996
-
 
 
997
  return { found: true, html: primary, sources, note, raw: data };
998
- } catch (err) {
999
- return { found: false, html: '<p>Failed to fetch from DuckDuckGo (possibly blocked by CORS). Try again or use a different query.</p>', sources: [], note: 'DuckDuckGo', raw: null };
1000
- }
1001
  }
1002
  }
1003
-
1004
  class WikipediaSource extends DataSource {
1005
- constructor() {
1006
- super('Wikipedia', '📚', 'wiki');
1007
- }
1008
-
1009
- canHandle() {
1010
- return /\b(who is|who was|what is|what was|definition|define|history|historical|article)\b/i.test(lastQuery) ||
1011
- /\b[a-zA-Z]{3,}\b/.test(lastQuery);
1012
- }
1013
-
1014
  getPriority() { return 35; }
1015
-
1016
  async search(query, signal) {
1017
  try {
1018
- // Try primary summary API
1019
  const title = query.replace(/^(who is|who was|what is|what was|definition|define)\s+/i, '').trim();
1020
  const searchUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
1021
  const data = await fetchWithCorsProxies(searchUrl, signal);
1022
 
1023
  if (data.extract) {
1024
  const sources = [];
1025
- if (data.content_urls?.desktop?.page) {
1026
- sources.push({ href: data.content_urls.desktop.page, label: 'Wikipedia Article', multi: true });
1027
- }
1028
-
1029
  let html = `<p>${escapeHTML(data.extract)}</p>`;
1030
- if (data.extract_html) {
1031
- html = data.extract_html;
1032
- }
1033
- if (data.thumbnail?.source) {
1034
- html = `<img src="${data.thumbnail.source}" alt="${escapeHTML(data.title || 'Image')}" style="max-width: 200px; border-radius: 8px; margin-bottom: 10px;" /><br/>${html}`;
1035
- }
1036
- return {
1037
- found: true,
1038
- html,
1039
- sources,
1040
- note: data.title ? `Wikipedia: ${data.title}` : 'Wikipedia Article',
1041
- raw: data
1042
- };
1043
  }
1044
  return { found: false, html: '<p>No Wikipedia article found.</p>', sources: [], note: 'Wikipedia', raw: data };
1045
- } catch (err) {
1046
- return { found: false, html: '<p>Error fetching Wikipedia data.</p>', sources: [], note: 'Wikipedia', raw: null };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8">
5
  <title>Enhanced Multi-Source AI Chatbot</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <meta name="description" content="Multi-source chatbot with DuckDuckGo, Wikipedia, Weather APIs, and offline fallbacks">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
 
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
11
  <style>
12
  :root {
13
+ --bg: #0b1020; --bg2: #0f1630; --panel: #0f1732; --text: #e7ecff; --muted: #a3b1d9;
14
+ --accent: #6ea8fe; --accent2: #8ad1ff; --bubble-user: #1a254f; --bubble-bot: #141e3d;
15
+ --shadow: 0 10px 30px rgba(0,0,0,.35); --radius: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
 
17
  * { box-sizing: border-box }
18
+ body {
19
+ margin: 0; padding: 0; height: 100%; font-family: Inter, system-ui, -apple-system, sans-serif;
20
+ color: var(--text); background: radial-gradient(1200px 800px at 10% -20%, #20306a 0%, transparent 60%),
 
 
 
 
 
 
21
  radial-gradient(900px 700px at 110% 10%, #1b3c7a 0%, transparent 60%),
22
  radial-gradient(700px 900px at 50% 120%, #1a365d 0%, transparent 60%),
23
+ linear-gradient(180deg, var(--bg), var(--bg2)); background-attachment: fixed;
 
24
  }
25
+ .app { display: flex; flex-direction: column; min-height: 100%; max-width: 1100px; margin: 0 auto; }
 
 
 
 
 
 
 
 
 
 
 
26
  header {
27
+ position: sticky; top: 0; z-index: 10; backdrop-filter: saturate(180%) blur(8px);
28
+ background: linear-gradient(180deg, rgba(10,16,34,.8), rgba(10,16,34,.35));
29
+ border-bottom: 1px solid rgba(255,255,255,.06);
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
+ .topbar { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; gap: 12px; }
32
+ .brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
 
 
 
 
 
 
33
  .logo {
34
+ width: 36px; height: 36px; border-radius: 10px;
 
 
35
  background: conic-gradient(from 210deg, #6ea8fe, #8ad1ff, #6ea8fe);
36
+ box-shadow: 0 6px 16px rgba(110,168,254,.35), inset 0 0 18px rgba(255,255,255,.2); position: relative;
 
37
  }
 
38
  .logo::after {
39
+ content: ""; position: absolute; inset: 3px; border-radius: 8px;
40
+ background: radial-gradient(120px 80px at 30% 20%, rgba(255,255,255,.5), transparent 40%),
41
+ linear-gradient(180deg, rgba(255,255,255,.12), rgba(0,0,0,.18));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
+ .title { font-weight: 800; letter-spacing: .2px; font-size: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
44
+ .subtitle { font-size: 12px; color: var(--muted); }
45
+ .links { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
46
  .btn {
47
+ appearance: none; border: none; cursor: pointer; font: inherit; color: inherit; padding: 10px 14px;
48
+ border-radius: 10px; background: #1a254f; color: #dfe8ff; display: inline-flex; align-items: center; gap: 8px;
49
+ border: 1px solid rgba(255,255,255,.08); transition: transform .08s ease, background .2s ease, border-color .2s ease, opacity .2s ease;
50
+ }
51
+ .btn:hover { transform: translateY(-1px); border-color: rgba(255,255,255,.18) }
52
+ .btn.secondary { background: #0f1732 }
53
+ .btn.ghost { background: transparent; border-color: rgba(255,255,255,.1) }
54
+ .btn:disabled { opacity: .6; cursor: not-allowed; transform: none }
55
+ .icon { width: 18px; height: 18px; display: inline-block; }
56
+ main { flex: 1; display: flex; flex-direction: column; padding: 12px; }
57
+ .chat { flex: 1; overflow: auto; padding: 12px; padding-bottom: 24px; scroll-behavior: smooth; }
58
+ .messages { display: flex; flex-direction: column; gap: 12px; }
59
+ .row { display: flex; gap: 10px; align-items: flex-end; }
60
+ .row.user { justify-content: flex-end }
61
+ .row.bot { justify-content: flex-start }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  .avatar {
63
+ width: 32px; height: 32px; border-radius: 50%; background: #172046; display: flex; align-items: center; justify-content: center;
64
+ font-size: 14px; font-weight: 700; border: 1px solid rgba(255,255,255,.08); box-shadow: var(--shadow); flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
+ .avatar.bot { background: radial-gradient(100px 80px at 20% 10%, #2a3d7a, #192555) }
67
+ .avatar.user { background: radial-gradient(120px 90px at 30% 10%, #1a2c5f, #0e1736) }
 
 
 
68
  .bubble {
69
+ max-width: min(800px, 86vw); padding: 12px 14px; border-radius: var(--radius);
70
+ border: 1px solid rgba(255,255,255,.08); box-shadow: var(--shadow); line-height: 1.55; position: relative;
71
+ word-wrap: break-word; overflow-wrap: anywhere;
72
+ }
73
+ .bubble.user { background: linear-gradient(180deg, #152152, #101a3e); border-top-right-radius: 6px }
74
+ .bubble.bot { background: linear-gradient(180deg, #111a3b, #0c1431); border-bottom-left-radius: 6px }
75
+ .meta { font-size: 12px; color: var(--muted); margin-bottom: 6px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
76
+ .badge { padding: 2px 8px; border-radius: 999px; background: #0c1330; border: 1px solid rgba(255,255,255,.08); font-size: 11px; color: #c7d5ff; }
77
+ .answer { font-size: 15px; color: #e9efff }
78
+ .answer p { margin: 0 0 10px 0 }
79
+ .answer ul, .answer ol { margin: 6px 0 10px 18px }
80
+ .answer a { color: var(--accent2); text-decoration: underline }
81
+ .sources { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  .source {
83
+ font-size: 12px; color: #c7d5ff; background: #0c1330; border: 1px solid rgba(255,255,255,.08);
84
+ padding: 4px 8px; border-radius: 8px;
 
 
 
 
85
  }
86
+ .source.multi { background: #0d1b3a; border-color: #4a90e2 }
 
 
 
 
 
87
  .composer {
88
+ position: sticky; bottom: 0; z-index: 9; display: flex; gap: 10px; align-items: flex-end; padding: 12px;
89
+ background: linear-gradient(180deg, rgba(10,16,34,.0), rgba(10,16,34,.85)); backdrop-filter: blur(8px);
90
+ border-top: 1px solid rgba(255,255,255,.06);
 
 
 
 
 
 
 
91
  }
 
92
  .input-wrap {
93
+ flex: 1; display: flex; gap: 10px; align-items: flex-end; background: linear-gradient(180deg, #0e1633, #0b122b);
94
+ border: 1px solid rgba(255,255,255,.08); border-radius: 14px; padding: 8px; box-shadow: var(--shadow);
 
 
 
 
 
 
 
95
  }
 
96
  textarea {
97
+ flex: 1; resize: none; border: none; outline: none; background: transparent; color: var(--text); font: inherit;
98
+ line-height: 1.4; max-height: 200px; min-height: 40px; padding: 8px 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
+ .actions { display: flex; gap: 8px; align-items: center; }
101
  .small-btn {
102
+ width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.1); background: #0e1633;
103
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
 
 
 
 
 
 
 
104
  transition: transform .08s ease, border-color .2s ease, background .2s ease;
105
  }
106
+ .small-btn:hover { transform: translateY(-1px); border-color: rgba(255,255,255,.2) }
107
+ .small-btn:disabled { opacity: .6; cursor: not-allowed; transform: none }
 
 
 
 
 
 
 
 
 
 
108
  .suggestions {
109
+ position: absolute; left: 12px; right: 12px; bottom: 64px; display: flex; flex-wrap: wrap; gap: 8px; pointer-events: none;
 
 
 
 
 
 
 
110
  }
 
111
  .chip {
112
+ pointer-events: auto; background: #0d1431; border: 1px solid rgba(255,255,255,.1); color: #d8e4ff;
113
+ padding: 6px 10px; border-radius: 999px; font-size: 12px; cursor: pointer;
 
 
 
 
 
 
114
  transition: transform .08s ease, background .2s ease, border-color .2s ease;
115
  }
116
+ .chip:hover { transform: translateY(-1px); background: #121c44; border-color: rgba(255,255,255,.2) }
117
+ .typing { display: inline-flex; align-items: center; gap: 6px; }
118
+ .dot { width: 6px; height: 6px; background: #c7d5ff; border-radius: 50%; opacity: .8; animation: blink 1.4s infinite ease-in-out; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  .dot:nth-child(2) { animation-delay: .2s }
120
  .dot:nth-child(3) { animation-delay: .4s }
121
+ @keyframes blink { 0%,80%,100% { transform: translateY(0); opacity: .5 } 40% { transform: translateY(-3px); opacity: 1 } }
 
 
 
 
 
122
  .hint { font-size: 12px; color: var(--muted); padding: 4px 2px 0 2px; }
123
+ .footer-note { text-align: center; font-size: 12px; color: var(--muted); padding: 10px 0 18px; }
 
 
 
 
 
 
 
124
  .data-source-indicator {
125
+ display: inline-flex; align-items: center; gap: 4px; font-size: 11px; padding: 2px 6px; border-radius: 6px;
126
+ background: rgba(74, 144, 226, 0.2); border: 1px solid rgba(74, 144, 226, 0.4);
127
+ }
128
+ .data-source-indicator.wiki { background: rgba(74, 226, 144, 0.2); border-color: rgba(74, 226, 144, 0.4); }
129
+ .data-source-indicator.weather { background: rgba(250, 204, 21, 0.2); border-color: rgba(250, 204, 21, 0.4); }
130
+ .data-source-indicator.calc { background: rgba(168, 85, 247, 0.2); border-color: rgba(168, 85, 247, 0.4); }
131
+ .data-source-indicator.offline { background: rgba(248, 113, 113, 0.2); border-color: rgba(248, 113, 113, 0.4); }
132
+ .source-badge { font-size: 10px; padding: 1px 4px; border-radius: 3px; background: rgba(255,255,255,0.1); margin-left: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  .mono {
134
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px;
135
+ background: #0b1434; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 10px; overflow: auto;
136
+ }
137
+ .search-results { background: #0d1431; border: 1px solid rgba(255,255,255,.1); border-radius: 8px; padding: 10px; margin: 8px 0; }
138
+ .search-result { margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,.05); }
139
+ .search-result:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
140
+ .search-result-title { font-weight: 600; color: #8ad1ff; text-decoration: none; }
141
+ .search-result-title:hover { text-decoration: underline; }
142
+ .search-result-snippet { font-size: 13px; color: #c7d5ff; margin-top: 2px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  .status {
144
+ position: fixed; bottom: 10px; right: 12px; background: rgba(12,19,48,0.8);
145
+ border: 1px solid rgba(255,255,255,.1); padding: 6px 10px; border-radius: 999px; font-size: 12px; color: var(--muted);
146
+ display: flex; align-items: center; gap: 8px; backdrop-filter: blur(6px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }
148
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #facc15; box-shadow: 0 0 8px #facc15; }
149
+ .status-dot.ok { background: #4ade80; box-shadow: 0 0 8px #4ade80; }
150
+ .status-dot.err { background: #f87171; box-shadow: 0 0 8px #f87171; }
 
 
151
  @media (max-width: 720px) {
152
  .topbar { padding: 12px }
153
+ .title { font-size: 17px }
154
  .links { display: none }
155
  .bubble { max-width: min(760px, 92vw) }
156
  .suggestions { bottom: 60px }
157
  }
 
 
158
  .chat::-webkit-scrollbar { width: 10px }
159
+ .chat::-webkit-scrollbar-thumb { background: #16214a; border-radius: 10px; border: 2px solid transparent; background-clip: padding-box; }
 
 
 
 
 
160
  .chat::-webkit-scrollbar-track { background: transparent }
161
+ .fade-in { animation: fadeIn .22s ease-out; }
162
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: translateY(0) } }
 
 
 
 
 
 
 
 
163
  </style>
164
  </head>
 
165
  <body>
166
  <div class="app">
167
  <header>
168
  <div class="topbar">
169
  <div class="brand">
170
+ <div class="logo"></div>
171
+ <div>
172
  <div class="title">Enhanced Multi-Source AI Chatbot</div>
173
  <div class="subtitle">DuckDuckGo, Wikipedia, Weather, Calculator + Offline Fallbacks</div>
174
  </div>
175
  </div>
176
  <div class="links">
177
+ <a class="btn ghost" href="https://duckduckgo.com/" target="_blank" rel="noopener">
178
+ <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 2a10 10 0 1 0 .001 20.001A10 10 0 0 0 12 2Zm0 0c2.5 0 4.5 5 4.5 5s-2 5-4.5 5-4.5-5-4.5-5 2-5 4.5-5Zm0 10c4.5 0 6 5 6 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
 
 
179
  DuckDuckGo
180
  </a>
181
+ <a class="btn ghost" href="https://en.wikipedia.org/" target="_blank" rel="noopener">
182
+ <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M4 4h16v16H4z" stroke="currentColor" stroke-width="2"/><path d="M7 8h10M7 12h10M7 16h6" stroke="currentColor" stroke-width="2"/></svg>
 
 
 
183
  Wikipedia
184
  </a>
185
+ <button id="exportBtn" class="btn secondary" title="Export chat">
186
  <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>
187
  Export
188
  </button>
 
190
  <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M3 6h18M8 6v12m8-12v12M5 6l1 14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2L19 6M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
191
  Clear
192
  </button>
193
+ <a class="btn" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener">Built with anycoder</a>
 
 
194
  </div>
195
  </div>
196
  </header>
 
197
  <main>
198
  <div class="chat" id="chat">
199
  <div class="messages" id="messages">
200
  <div class="row bot fade-in">
201
  <div class="avatar bot">🤖</div>
202
  <div class="bubble bot">
203
+ <div class="meta"><span class="badge">Bot</span><span>Multi-Source AI Assistant</span></div>
 
 
 
204
  <div class="answer">
205
  <p>Hi! I'm your enhanced AI assistant with multiple data sources including DuckDuckGo Instant Answers, Wikipedia, Weather APIs, Calculator, and robust offline fallbacks.</p>
206
  <p class="hint">Try asking about: facts, definitions, weather, time zones, calculations, historical events, or general knowledge.</p>
 
209
  </div>
210
  </div>
211
  </div>
 
212
  <div class="suggestions" id="suggestions"></div>
 
213
  <div class="composer">
214
  <div class="input-wrap">
215
  <textarea id="prompt" rows="1" placeholder="Ask me anything... (Shift+Enter = newline)"></textarea>
 
224
  </div>
225
  </div>
226
  </main>
227
+ <div class="footer-note">Enhanced with multiple data sources: DuckDuckGo, Wikipedia, Open-Meteo Weather API, Calculator, and offline knowledge.</div>
 
 
 
228
  </div>
 
229
  <div class="status" id="status">
230
  <span class="status-dot" id="statusDot"></span>
231
  <span id="statusText">Checking network...</span>
232
  </div>
 
233
  <script>
234
  // Utilities
235
+ const $ = (sel, el=document) => el.querySelector(sel), $$ = (sel, el=document) => [...el.querySelectorAll(sel)];
236
+ const escapeHTML = s => s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
 
 
 
237
  const nowTime = () => new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
238
+
239
  function updateTextareaHeight() {
240
+ const el = $('#prompt'); el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 200) + 'px';
 
 
241
  }
242
+
243
  // DOM elements
244
+ const chatEl = $('#chat'), messagesEl = $('#messages'), promptEl = $('#prompt'), sendBtn = $('#sendBtn'), stopBtn = $('#stopBtn'), suggestionsEl = $('#suggestions'), clearBtn = $('#clearBtn'), exportBtn = $('#exportBtn'), statusDot = $('#statusDot'), statusText = $('#statusText');
245
+
 
 
 
 
 
 
 
 
 
246
  // State
247
+ let history = [], abortController = null, lastQuery = '', offlineMode = false;
248
+
 
 
 
249
  // Network indicator
250
  function updateNetworkStatus() {
251
+ if (navigator.onLine) { statusDot.className = 'status-dot ok'; statusText.textContent = 'Online'; offlineMode = false; }
252
+ else { statusDot.className = 'status-dot err'; statusText.textContent = 'Offline'; offlineMode = true; }
 
 
 
 
 
 
 
253
  }
254
+ window.addEventListener('online', updateNetworkStatus); window.addEventListener('offline', updateNetworkStatus); updateNetworkStatus();
255
+
 
 
256
  // Suggestions
257
+ const defaultChips = ['weather in Tokyo', 'time in London', 'define artificial intelligence', 'who was Einstein', 'sqrt(144)', 'USD to EUR', 'prime factors of 91', 'Python programming history', 'what is machine learning', 'how tall is Mount Everest', 'capital of France', 'latest news about AI', 'JavaScript vs Python', 'quantum computing explained'];
258
+
 
 
 
 
 
259
  function renderSuggestions(items=defaultChips){
260
+ suggestionsEl.innerHTML = ''; items.forEach(text => {
261
+ const chip = document.createElement('button'); chip.className = 'chip'; chip.textContent = text;
262
+ chip.onclick = () => { promptEl.value = text; updateTextareaHeight(); promptEl.focus(); };
 
 
 
 
 
 
 
263
  suggestionsEl.appendChild(chip);
264
  });
265
  }
266
+
267
  function buildPromptChips(q){
268
+ q = q.trim(); const chips = [];
 
269
  const hasWeather = /\b(weather|temperature|forecast)\b/i.test(q);
270
  const hasDefine = /\b(define|meaning|definition|what does)\b/i.test(q);
271
  const hasConvert = /\b(to|in|°)\b/i.test(q) || /\b(c|f|cm|inch|kg|lb)\b/.test(q);
 
274
  const hasCalc = /[\d\+\-\*\/\^\(\)\.]/.test(q);
275
  const hasHistory = /\b(history|historical)\b/i.test(q);
276
  const hasScience = /\b(science|scientific)\b/i.test(q);
277
+
278
+ if (q && !hasWeather) chips.push(q.replace(/\b(what is|what's)\b/i,'') + ' weather');
279
  if (q && !hasDefine) chips.push('define ' + q.replace(/\b(define|meaning|definition|what does)\b/i,'').trim());
280
  if (q && !hasConvert && /(?:\d|\b)(°\s?[cf]|celsius|fahrenheit|cm|inch|kg|lb)\b/i.test(q)) chips.push(q + ' to fahrenheit');
281
  if (q && !hasTime) chips.push('time in ' + q.replace(/\b(what is|what's|time in|date|datetime)\b/ig,'').trim());
 
283
  if (q && !hasCalc) chips.push(q.replace(/\b(calculate|compute|solve)\b/i,'').trim());
284
  if (q && !hasHistory) chips.push(q + ' history');
285
  if (q && !hasScience) chips.push(q + ' science');
286
+
287
  const unique = [...new Set(chips)].filter(Boolean).slice(0,6);
288
+ if (unique.length === 0) renderSuggestions(defaultChips); else renderSuggestions(unique);
 
289
  }
290
+
291
  // Message helpers
292
  function addMessage(role, contentHTML, meta = {}){
293
+ const row = document.createElement('div'); row.className = `row ${role} fade-in`;
294
+ const avatar = document.createElement('div'); avatar.className = `avatar ${role}`; avatar.textContent = role === 'bot' ? '🤖' : '🙂';
295
+ const bubble = document.createElement('div'); bubble.className = `bubble ${role}`;
296
+ const metaEl = document.createElement('div'); metaEl.className = 'meta';
297
+ const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = role === 'bot' ? (meta.badge || 'AI Assistant') : 'You';
 
 
 
 
 
 
 
 
 
 
298
  metaEl.appendChild(badge);
 
299
  if (meta.source) {
300
+ const sourceEl = document.createElement('span'); sourceEl.className = `data-source-indicator ${meta.source.className || ''}`;
 
301
  sourceEl.innerHTML = `${meta.source.icon || '📡'} ${meta.source.name}`;
302
  metaEl.appendChild(sourceEl);
303
  }
304
+ if (meta.note){ const note = document.createElement('span'); note.textContent = meta.note; note.style.color = 'var(--muted)'; metaEl.appendChild(note); }
305
+ const answer = document.createElement('div'); answer.className = 'answer'; answer.innerHTML = contentHTML;
306
+ bubble.appendChild(metaEl); bubble.appendChild(answer);
 
 
 
 
 
 
 
 
 
 
 
 
307
  if (meta.sources && meta.sources.length){
308
+ const sources = document.createElement('div'); sources.className = 'sources';
 
309
  meta.sources.forEach(src => {
310
+ const s = document.createElement('a'); s.className = `source ${src.multi ? 'multi' : ''}`; s.href = src.href; s.target = '_blank'; s.rel = 'noopener noreferrer'; s.textContent = src.label || src.href; sources.appendChild(s);
 
 
 
 
 
 
311
  });
312
  bubble.appendChild(sources);
313
  }
314
+ row.appendChild(avatar); row.appendChild(bubble); messagesEl.appendChild(row); chatEl.scrollTop = chatEl.scrollHeight; return bubble;
 
 
 
 
 
315
  }
316
+
317
  function addTyping(){
318
+ const row = document.createElement('div'); row.className = 'row bot fade-in'; row.dataset.typing = '1';
319
+ const avatar = document.createElement('div'); avatar.className = 'avatar bot'; avatar.textContent = '🤖';
320
+ const bubble = document.createElement('div'); bubble.className = 'bubble bot';
321
+ const metaEl = document.createElement('div'); metaEl.className = 'meta';
322
+ const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = 'AI Assistant';
 
 
 
 
 
 
 
 
323
  metaEl.appendChild(badge);
324
+ const answer = document.createElement('div'); answer.className = 'answer';
 
325
  answer.innerHTML = `<span class="typing"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span>`;
326
+ bubble.appendChild(metaEl); bubble.appendChild(answer);
327
+ row.appendChild(avatar); row.appendChild(bubble); messagesEl.appendChild(row); chatEl.scrollTop = chatEl.scrollHeight; return row;
 
 
 
 
 
 
 
 
 
328
  }
329
+ function removeTyping(){ const t = $('[data-typing="1"]', messagesEl); if (t) t.remove(); }
330
+
331
  // CORS proxy list
332
  const CORS_PROXIES = [
333
+ target => `https://cors.isomorphic-git.org/${target}`,
334
+ targetapi.codetabs => `https://.com/v1/proxy?quest=${encodeURIComponent(target)}`,
335
+ target => `https://corsproxy.io/?${encodeURIComponent(target)}`
336
  ];
337
+
338
  async function fetchWithCorsProxies(targetUrl, signal){
339
  let lastErr = null;
340
  for (const buildProxy of CORS_PROXIES){
 
345
  const contentType = res.headers.get('content-type') || '';
346
  const text = await res.text();
347
  let data = text;
348
+ if (contentType.includes('application/json')) { try { data = JSON.parse(text); } catch {} }
 
 
349
  return data;
350
+ }catch(err){ lastErr = err; }
 
 
351
  }
352
  throw lastErr || new Error('All CORS proxies failed');
353
  }
354
+
355
  // Data Source Classes
356
  class DataSource {
357
+ constructor(name, icon, className) { this.name = name; this.icon = icon; this.className = className; }
 
 
 
 
358
  async search(query, signal) { throw new Error('Not implemented'); }
359
  getPriority() { return 50; }
360
  canHandle() { return false; }
361
  }
362
+
363
  class OfflineSource extends DataSource {
364
+ constructor() { super('Offline', '💾', 'offline'); }
 
 
 
365
  canHandle() { return offlineMode; }
366
+ getPriority() { return 5; }
 
 
367
  async search(query, signal) {
 
368
  const kb = LocalKnowledgeBase;
369
  const found = kb.search(query);
370
  if (found) {
371
+ return { found: true, html: found.html, sources: found.sources || [], note: found.note || 'Offline knowledge', raw: { query } };
 
 
 
 
 
 
372
  }
373
+ return { found: false, html: '<p>I\'m currently offline and don\'t have information about that. Please try a rephrase or check your network connection.</p>', sources: [], note: 'Offline', raw: { query } };
 
 
 
 
 
 
374
  }
375
  }
376
+
377
  class DuckDuckGoSource extends DataSource {
378
+ constructor() { super('DuckDuckGo', '🦆', 'ddg'); }
379
+ buildDDGUrl(query){ const url = new URL('https://api.duckduckgo.com/'); url.searchParams.set('q', query); url.searchParams.set('format', 'json'); url.searchParams.set('no_html', '1'); url.searchParams.set('no_redirect', '1'); url.searchParams.set('skip_disambig', '1'); return url.toString(); }
380
+ canHandle() { return true; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  getPriority() { return 40; }
 
382
  async search(query, signal) {
383
  const targetUrl = this.buildDDGUrl(query);
384
  try {
385
  const data = await fetchWithCorsProxies(targetUrl, signal);
386
  const sources = [];
387
  if (Array.isArray(data.Results)) {
388
+ data.Results.forEach(r => { if (r && r.FirstURL && r.Text) sources.push({ href: r.FirstURL, label: new URL(r.FirstURL).hostname.replace('www.','') }); });
 
 
389
  }
390
  if (data.AbstractURL) sources.push({ href: data.AbstractURL, label: new URL(data.AbstractURL).hostname.replace('www.','') });
391
  if (data.Answer && data.AnswerURL) sources.push({ href: data.AnswerURL, label: new URL(data.AnswerURL).hostname.replace('www.','') });
392
+
393
+ let primary = ''; let note = '';
394
+
395
+ if (data.Answer && data.Answer.trim()){ primary = data.Answer; note = 'Instant Answer'; }
396
+ else if (data.Definition && data.Definition.trim()){ primary = data.Definition; note = data.DefinitionSource ? ('Definition • ' + data.DefinitionSource) : 'Definition'; if (data.DefinitionURL) sources.push({ href: data.DefinitionURL, label: new URL(data.DefinitionURL).hostname.replace('www.','') }); }
397
+ else if (data.AbstractText && data.AbstractText.trim()){ primary = data.AbstractText; note = data.Heading ? ('About: ' + data.Heading) : (data.AbstractSource ? data.AbstractSource : 'Abstract'); }
398
+ else if (data.RelatedTopics && data.RelatedTopics.length){
 
 
 
 
 
 
 
 
399
  const items = [];
400
  for (const t of data.RelatedTopics){
401
+ if (t && t.Text && t.FirstURL) { items.push(`<li><a href="${t.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(t.Text)}</a></li>`); }
402
+ else if (Array.isArray(t.Topics)) {
403
+ for (const tt of t.Topics){ if (tt && tt.Text && tt.FirstURL) { items.push(`<li><a href="${tt.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(tt.Text)}</a></li>`); } }
 
 
 
 
 
404
  }
405
  if (items.length >= 6) break;
406
  }
407
+ if (items.length){ primary = `<p>Here are some related topics I found:</p><ul>${items.join('')}</ul>`; note = 'Related topics'; }
 
 
 
 
 
 
 
408
  }
409
+
410
+ if (!primary) { return { found: false, html: '<p>No direct answer from DuckDuckGo.</p>', sources, note: 'DuckDuckGo', raw: data }; }
411
+
412
  return { found: true, html: primary, sources, note, raw: data };
413
+ } catch (err) { return { found: false, html: '<p>Failed to fetch from DuckDuckGo (possibly blocked by CORS). Try again or use a different query.</p>', sources: [], note: 'DuckDuckGo', raw: null }; }
 
 
414
  }
415
  }
416
+
417
  class WikipediaSource extends DataSource {
418
+ constructor() { super('Wikipedia', '📚', 'wiki'); }
419
+ canHandle() { return /\b(who is|who was|what is|what was|definition|define|history|historical|article)\b/i.test(lastQuery) || /\b[a-zA-Z]{3,}\b/.test(lastQuery); }
 
 
 
 
 
 
 
420
  getPriority() { return 35; }
 
421
  async search(query, signal) {
422
  try {
 
423
  const title = query.replace(/^(who is|who was|what is|what was|definition|define)\s+/i, '').trim();
424
  const searchUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
425
  const data = await fetchWithCorsProxies(searchUrl, signal);
426
 
427
  if (data.extract) {
428
  const sources = [];
429
+ if (data.content_urls?.desktop?.page) { sources.push({ href: data.content_urls.desktop.page, label: 'Wikipedia Article', multi: true }); }
430
+
 
 
431
  let html = `<p>${escapeHTML(data.extract)}</p>`;
432
+ if (data.extract_html) { html = data.extract_html; }
433
+ if (data.thumbnail?.source) { html = `<img src="${data.thumbnail.source}" alt="${escapeHTML(data.title || 'Image')}" style="max-width: 200px; border-radius: 8px; margin-bottom: 10px;" /><br/>${html}`; }
434
+ return { found: true, html, sources, note: data.title ? `Wikipedia: ${data.title}` : 'Wikipedia Article', raw: data };
 
 
 
 
 
 
 
 
 
 
435
  }
436
  return { found: false, html: '<p>No Wikipedia article found.</p>', sources: [], note: 'Wikipedia', raw: data };
437
+ } catch (err) { return { found: false, html: '<p>Error fetching Wikipedia data.</p>', sources: [], note: 'Wikipedia', raw: null }; }
438
+ }
439
+ }
440
+
441
+ class WeatherSource extends DataSource {
442
+ constructor() { super('Weather', '🌤️', 'weather'); }
443
+ canHandle() { return /\b(weather|temperature|forecast)\b/i.test(lastQuery); }
444
+ getPriority() { return 30; }
445
+ async search(query, signal) {
446
+ const cityMatch = query.match(/\b(?:weather|temperature|forecast)\s+(?:in|at|for)?\s*([a-zA-Z\s,]+)/i);
447
+ const city = cityMatch ? cityMatch[1].trim() : '';
448
+ if (!city) { return { found: false, html: '<p>Please specify a city for weather information (e.g., "weather in Tokyo").</p>', sources: [], note: 'Weather', raw: null }; }
449
+
450
+ try {
451
+ const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`;
452
+ const geoData = await fetch(geoUrl, { signal }).then(r => r.json());
453
+ if (!geoData.results || geoData.results.length === 0) { return { found: false, html: `<p>City "${city}" not found.</p>`, sources: [], note: 'Weather', raw: null }; }
454
+
455
+ const { latitude, longitude, name, country_code, timezone } = geoData.results[0];
456
+ const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=${timezone}`;
457
+ const weatherData = await fetch(weatherUrl, { signal }).then(r => r.json());
458
+
459
+ if (weatherData.current_weather) {
460
+ const current = weatherData.current_weather;
461
+ const html = `
462
+ <p><strong>Weather in ${name}, ${country_code}</strong></p>
463
+ <ul>
464
+ <li>Temperature: ${current.temperature}°C (${Math.round(current.temperature * 9/5 + 32)}°F)</li>
465
+ <li>Wind Speed: ${current.windspeed} km/h</li>
466
+ <li>Weather Code: ${current.weathercode}</li>
467
+ <li>Time: ${current.time}</li>
468
+ </ul>
469
+ <p><em>Data provided by Open-Meteo</em></p>
470
+ `;
471
+ return { found: true, html, sources: [{ href: `https://open-meteo.com/`, label: 'Open-Meteo API' }], note: 'Weather Data', raw: weatherData };
472
+ }
473
+ return { found: false, html: '<p>Weather data not available.</p>', sources: [], note: 'Weather', raw: null };
474
+ } catch (err) { return { found: false, html: '<p>Error fetching weather data.</p>', sources: [], note: 'Weather', raw: null }; }
475
+ }
476
+ }
477
+
478
+ class CalculatorSource extends DataSource {
479
+ constructor() { super('Calculator', '🧮', 'calc'); }
480
+ canHandle() { return /[\d\+\-\*\/\^\(\)\.]/.test(lastQuery) && !/\b(what is|who|where|when|why|how)\b/i.test(lastQuery); }
481
+ getPriority() { return 25; }
482
+ async search(query, signal) {
483
+ try {
484
+ const clean = query.replace(/[^0-9+\-*/().\s]/g, '');
485
+ if (!clean.trim()) { return { found: false, html: '<p>No calculation detected.</p>', sources: [], note: 'Calculator', raw: null }; }
486
+
487
+ // Safe evaluation using Function constructor
488
+ const result = Function(`"use strict"; return (${clean})`)();
489
+ if (typeof result === 'number' && isFinite(result)) {
490
+ const html = `<p><strong>Calculation:</strong> ${escapeHTML(clean)} = <strong>${result}</strong></p>`;
491
+ return { found: true, html, sources: [], note: 'Calculator', raw: { expression: clean, result } };
492
+ }
493
+ return { found: false, html: '<p>Invalid expression.</p>', sources: [], note: 'Calculator', raw: null };
494
+ } catch (err) { return { found: false, html: '<p>Calculation error.</p>', sources: [], note: 'Calculator', raw: null }; }
495
+ }
496
+ }
497
+
498
+ // Local Knowledge Base
499
+ const LocalKnowledgeBase = {
500
+ data: [
501
+ { query: 'hello|hi|hey|greetings', html: '<p>Hello! I\'m an AI assistant that can help you with facts, weather, calculations, and more. What would you like to know?</p>', note: 'Greeting' },
502
+ { query: 'who are you|what are you|your name', html: '<p>I\'m an AI assistant with access to multiple data sources including DuckDuckGo, Wikipedia, weather APIs, and a built-in knowledge base for offline use.</p>', note: 'Identity' },
503
+ { query: 'artificial intelligence|ai|machine learning', html: '<p>Artificial Intelligence (AI) is a branch of computer science focused on creating systems that can perform tasks typically requiring human intelligence, such as learning, reasoning, problem-solving, and understanding natural language.</p>', note: 'Definition' },
504
+ { query: 'python programming', html: '<p>Python is a high-level, interpreted programming language known for its simple syntax and versatility. It\'s widely used in web development, data science, artificial intelligence, automation, and scientific computing.</p>', note: 'Programming' },
505
+ { query: 'javascript', html: '<p>JavaScript is a programming language that runs in web browsers and is essential for creating interactive web pages. It can also be used on servers (Node.js) and for mobile app development.</p>', note: 'Programming' }
506
+ ],
507
+ search(query) {
508
+ const lowerQuery = query.toLowerCase();
509
+ return this.data.find(item => new RegExp(item.query, 'i').test(lowerQuery));
510
+ }
511
+ };
512
+
513
+ // Initialize data sources
514
+ const sources = [new OfflineSource(), new CalculatorSource(), new WeatherSource(), new WikipediaSource(), new DuckDuckGoSource()];
515
+
516
+ // Search orchestrator
517
+ async function searchMultipleSources(query, signal) {
518
+ const availableSources = sources.filter(s => s.canHandle()).sort((a, b) => b.getPriority() - a.getPriority());
519
+
520
+ for (const source of availableSources) {
521
+ try {
522
+ const result = await source.search(query, signal);
523
+ if (result.found