AiCoderv2 commited on
Commit
9879274
·
verified ·
1 Parent(s): 44ebfb5

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +677 -19
index.html CHANGED
@@ -1,19 +1,677 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>DuckDuckGo AI Chatbot</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="description" content="A simple chatbot that answers using DuckDuckGo instant answers." />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root{
13
+ --bg: #0b1020;
14
+ --bg2: #0f1630;
15
+ --panel: #0f1732;
16
+ --panel-2: #111a3b;
17
+ --text: #e7ecff;
18
+ --muted: #a3b1d9;
19
+ --accent: #6ea8fe;
20
+ --accent-2: #8ad1ff;
21
+ --success: #4ade80;
22
+ --warning: #facc15;
23
+ --danger: #f87171;
24
+ --bubble-user: #1a254f;
25
+ --bubble-bot: #141e3d;
26
+ --shadow: 0 10px 30px rgba(0,0,0,.35);
27
+ --radius: 14px;
28
+ }
29
+ *{box-sizing:border-box}
30
+ html, body{
31
+ margin:0; padding:0; height:100%;
32
+ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol";
33
+ color: var(--text);
34
+ background:
35
+ radial-gradient(1200px 800px at 10% -20%, #20306a 0%, transparent 60%),
36
+ radial-gradient(900px 700px at 110% 10%, #1b3c7a 0%, transparent 60%),
37
+ radial-gradient(700px 900px at 50% 120%, #1a365d 0%, transparent 60%),
38
+ linear-gradient(180deg, var(--bg), var(--bg2));
39
+ background-attachment: fixed;
40
+ }
41
+ a{color: var(--accent)}
42
+ .app{
43
+ display:flex; flex-direction:column; min-height:100%;
44
+ width:100%; max-width:1100px; margin:0 auto;
45
+ }
46
+ header{
47
+ position: sticky; top:0; z-index:10;
48
+ backdrop-filter: saturate(180%) blur(8px);
49
+ background: linear-gradient(180deg, rgba(10,16,34,.8), rgba(10,16,34,.35));
50
+ border-bottom: 1px solid rgba(255,255,255,.06);
51
+ }
52
+ .topbar{
53
+ display:flex; align-items:center; justify-content:space-between;
54
+ padding:14px 18px; gap:12px;
55
+ }
56
+ .brand{
57
+ display:flex; align-items:center; gap:12px; min-width:0;
58
+ }
59
+ .logo{
60
+ width:36px; height:36px; border-radius:10px;
61
+ background: conic-gradient(from 210deg, #6ea8fe, #8ad1ff, #6ea8fe);
62
+ box-shadow: 0 6px 16px rgba(110,168,254,.35), inset 0 0 18px rgba(255,255,255,.2);
63
+ position:relative;
64
+ }
65
+ .logo::after{
66
+ content:""; position:absolute; inset:3px; border-radius:8px;
67
+ background: radial-gradient(120px 80px at 30% 20%, rgba(255,255,255,.5), transparent 40%),
68
+ linear-gradient(180deg, rgba(255,255,255,.12), rgba(0,0,0,.18));
69
+ }
70
+ .titles{
71
+ min-width:0;
72
+ }
73
+ .title{
74
+ font-weight:800; letter-spacing:.2px; font-size:18px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
75
+ }
76
+ .subtitle{
77
+ font-size:12px; color:var(--muted);
78
+ }
79
+ .links{
80
+ display:flex; gap:10px; align-items:center; flex-wrap:wrap;
81
+ }
82
+ .btn{
83
+ appearance:none; border:none; cursor:pointer; font: inherit; color:inherit;
84
+ padding:10px 14px; border-radius:10px; background: #1a254f; color:#dfe8ff;
85
+ display:inline-flex; align-items:center; gap:8px;
86
+ border:1px solid rgba(255,255,255,.08);
87
+ transition: transform .08s ease, background .2s ease, border-color .2s ease, opacity .2s ease;
88
+ }
89
+ .btn:hover{ transform: translateY(-1px); border-color: rgba(255,255,255,.18) }
90
+ .btn.secondary{ background: #0f1732; }
91
+ .btn.ghost{ background: transparent; border-color: rgba(255,255,255,.1) }
92
+ .btn:disabled{ opacity:.6; cursor:not-allowed; transform:none }
93
+ .icon{
94
+ width:18px; height:18px; display:inline-block;
95
+ }
96
+
97
+ main{
98
+ flex:1; display:flex; flex-direction:column; padding: 12px;
99
+ }
100
+ .chat{
101
+ flex:1; overflow:auto; padding: 12px; padding-bottom: 24px;
102
+ scroll-behavior:smooth;
103
+ }
104
+ .messages{
105
+ display:flex; flex-direction:column; gap:12px;
106
+ }
107
+ .row{
108
+ display:flex; gap:10px; align-items:flex-end;
109
+ }
110
+ .row.user{ justify-content:flex-end; }
111
+ .row.bot{ justify-content:flex-start; }
112
+ .avatar{
113
+ width:32px; height:32px; border-radius:50%; background:#172046; display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700;
114
+ border:1px solid rgba(255,255,255,.08);
115
+ box-shadow: var(--shadow);
116
+ flex-shrink:0;
117
+ }
118
+ .avatar.bot{
119
+ background: radial-gradient(100px 80px at 20% 10%, #2a3d7a, #192555);
120
+ }
121
+ .avatar.user{
122
+ background: radial-gradient(120px 90px at 30% 10%, #1a2c5f, #0e1736);
123
+ }
124
+ .bubble{
125
+ max-width: min(800px, 86vw);
126
+ padding: 12px 14px;
127
+ border-radius: var(--radius);
128
+ border: 1px solid rgba(255,255,255,.08);
129
+ box-shadow: var(--shadow);
130
+ line-height:1.55;
131
+ position:relative;
132
+ word-wrap:break-word;
133
+ overflow-wrap:anywhere;
134
+ }
135
+ .bubble.user{
136
+ background: linear-gradient(180deg, #152152, #101a3e);
137
+ border-top-right-radius: 6px;
138
+ }
139
+ .bubble.bot{
140
+ background: linear-gradient(180deg, #111a3b, #0c1431);
141
+ border-bottom-left-radius: 6px;
142
+ }
143
+ .bubble .meta{
144
+ font-size:12px; color:var(--muted); margin-bottom:6px; display:flex; gap:8px; align-items:center; flex-wrap:wrap;
145
+ }
146
+ .badge{
147
+ padding:2px 8px; border-radius: 999px; background:#0c1330; border:1px solid rgba(255,255,255,.08); font-size:11px; color:#c7d5ff;
148
+ }
149
+ .answer{
150
+ font-size:15px; color:#e9efff;
151
+ }
152
+ .answer p{ margin: 0 0 10px 0; }
153
+ .answer ul, .answer ol{ margin: 6px 0 10px 18px; }
154
+ .answer a{ color: var(--accent-2); text-decoration: underline; }
155
+ .sources{
156
+ display:flex; gap:8px; flex-wrap:wrap; margin-top:8px;
157
+ }
158
+ .source{
159
+ font-size:12px; color:#c7d5ff; background:#0c1330; border:1px solid rgba(255,255,255,.08); padding:4px 8px; border-radius:8px;
160
+ }
161
+ .composer{
162
+ position: sticky; bottom:0; z-index:9;
163
+ display:flex; gap:10px; align-items:flex-end; padding: 12px;
164
+ background: linear-gradient(180deg, rgba(10,16,34,.0), rgba(10,16,34,.85));
165
+ backdrop-filter: blur(8px);
166
+ border-top: 1px solid rgba(255,255,255,.06);
167
+ }
168
+ .input-wrap{
169
+ flex:1; display:flex; gap:10px; align-items:flex-end;
170
+ background: linear-gradient(180deg, #0e1633, #0b122b);
171
+ border:1px solid rgba(255,255,255,.08);
172
+ border-radius: 14px; padding: 8px;
173
+ box-shadow: var(--shadow);
174
+ }
175
+ textarea{
176
+ flex:1; resize:none; border:none; outline:none; background:transparent; color:var(--text);
177
+ font: inherit; line-height:1.4; max-height:200px; min-height:40px;
178
+ padding: 8px 10px;
179
+ }
180
+ .actions{ display:flex; gap:8px; align-items:center; }
181
+ .small-btn{
182
+ width:38px; height:38px; border-radius:10px; border:1px solid rgba(255,255,255,.1);
183
+ background: #0e1633; display:flex; align-items:center; justify-content:center; cursor:pointer;
184
+ transition: transform .08s ease, border-color .2s ease, background .2s ease;
185
+ }
186
+ .small-btn:hover{ transform: translateY(-1px); border-color: rgba(255,255,255,.2) }
187
+ .small-btn:disabled{ opacity:.6; cursor:not-allowed; transform:none }
188
+
189
+ .suggestions{
190
+ position:absolute; left:12px; right:12px; bottom: 64px;
191
+ display:flex; flex-wrap:wrap; gap:8px;
192
+ pointer-events:none;
193
+ }
194
+ .chip{
195
+ pointer-events:auto;
196
+ background:#0d1431; border:1px solid rgba(255,255,255,.1); color:#d8e4ff; padding:6px 10px; border-radius:999px; font-size:12px;
197
+ cursor:pointer; transition: transform .08s ease, background .2s ease, border-color .2s ease;
198
+ }
199
+ .chip:hover{ transform: translateY(-1px); background:#121c44; border-color: rgba(255,255,255,.2) }
200
+
201
+ .typing{
202
+ display:inline-flex; align-items:center; gap:6px;
203
+ }
204
+ .dot{
205
+ width:6px; height:6px; background:#c7d5ff; border-radius:50%; opacity:.8; animation: blink 1.4s infinite ease-in-out;
206
+ }
207
+ .dot:nth-child(2){ animation-delay:.2s }
208
+ .dot:nth-child(3){ animation-delay:.4s }
209
+ @keyframes blink {
210
+ 0%, 80%, 100% { transform: translateY(0); opacity:.5 }
211
+ 40% { transform: translateY(-3px); opacity:1 }
212
+ }
213
+
214
+ .hint{
215
+ font-size:12px; color:var(--muted); padding: 4px 2px 0 2px;
216
+ }
217
+
218
+ .footer-note{
219
+ text-align:center; font-size:12px; color:var(--muted); padding:10px 0 18px;
220
+ }
221
+
222
+ /* Responsive */
223
+ @media (max-width: 720px){
224
+ .topbar{ padding: 12px }
225
+ .titles .title{ font-size:17px }
226
+ .links{ display:none }
227
+ .bubble{ max-width: min(760px, 92vw) }
228
+ .suggestions{ bottom: 60px }
229
+ }
230
+
231
+ /* Scrollbar */
232
+ .chat::-webkit-scrollbar{ width:10px }
233
+ .chat::-webkit-scrollbar-thumb{
234
+ background: #16214a; border-radius:10px; border:2px solid transparent; background-clip: padding-box;
235
+ }
236
+ .chat::-webkit-scrollbar-track{ background: transparent }
237
+
238
+ /* Simple fade-in for messages */
239
+ .fade-in{
240
+ animation: fadeIn .22s ease-out;
241
+ }
242
+ @keyframes fadeIn{
243
+ from{ opacity:0; transform: translateY(6px) }
244
+ to{ opacity:1; transform: translateY(0) }
245
+ }
246
+
247
+ .mono{
248
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
249
+ font-size: 12px;
250
+ background: #0b1434; border:1px solid rgba(255,255,255,.08); border-radius:8px; padding:10px; overflow:auto;
251
+ }
252
+ </style>
253
+ </head>
254
+ <body>
255
+ <div class="app">
256
+ <header>
257
+ <div class="topbar">
258
+ <div class="brand">
259
+ <div class="logo" aria-hidden="true"></div>
260
+ <div class="titles">
261
+ <div class="title">DuckDuckGo Answer Bot</div>
262
+ <div class="subtitle">Ask anything. Powered by DuckDuckGo Instant Answers.</div>
263
+ </div>
264
+ </div>
265
+ <div class="links">
266
+ <a class="btn ghost" href="https://duckduckgo.com/" target="_blank" rel="noopener noreferrer">
267
+ <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>
268
+ DuckDuckGo
269
+ </a>
270
+ <button id="exportBtn" class="btn secondary" title="Export chat as JSON">
271
+ <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>
272
+ Export
273
+ </button>
274
+ <button id="clearBtn" class="btn" title="Clear chat">
275
+ <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>
276
+ Clear
277
+ </button>
278
+ <a class="btn" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener noreferrer" title="Built with anycoder">
279
+ Built with anycoder
280
+ </a>
281
+ </div>
282
+ </div>
283
+ </header>
284
+
285
+ <main>
286
+ <div class="chat" id="chat">
287
+ <div class="messages" id="messages">
288
+ <div class="row bot fade-in">
289
+ <div class="avatar bot">🤖</div>
290
+ <div class="bubble bot">
291
+ <div class="meta">
292
+ <span class="badge">Bot</span>
293
+ <span>DuckDuckGo Instant Answers</span>
294
+ </div>
295
+ <div class="answer">
296
+ <p>Hi! I’m your DuckDuckGo-powered assistant. Ask me anything: facts, definitions, unit conversions, weather, time, math, and more.</p>
297
+ <p class="hint">Tip: Try queries like “weather in Tokyo”, “define ubiquitous”, “2pm PST to CET”, or “what is 18 celsius in fahrenheit”.</p>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+
304
+ <div class="suggestions" id="suggestions"></div>
305
+
306
+ <div class="composer">
307
+ <div class="input-wrap">
308
+ <textarea id="prompt" rows="1" placeholder="Ask me anything... (Shift+Enter = newline)"></textarea>
309
+ <div class="actions">
310
+ <button class="small-btn" id="stopBtn" disabled title="Stop">
311
+ <svg class="icon" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"/></svg>
312
+ </button>
313
+ <button class="small-btn" id="sendBtn" title="Send (Enter)">
314
+ <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M5 12h7m0 0-4-4m4 4-4 4M19 12h-7m0 0 4-4m-4 4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
315
+ </button>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </main>
320
+
321
+ <div class="footer-note">
322
+ Answers come from DuckDuckGo Instant Answers. Some queries may have no result—try rephrasing.
323
+ </div>
324
+ </div>
325
+
326
+ <script>
327
+ // Utilities
328
+ const $ = (sel, el=document) => el.querySelector(sel);
329
+ const $$ = (sel, el=document) => [...el.querySelectorAll(sel)];
330
+ const escapeHTML = (s) => s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
331
+ const stripTags = (s) => s.replace(/<[^>]*>/g, '');
332
+ const truncate = (s, n=220) => s.length > n ? s.slice(0, n-1) + '…' : s;
333
+
334
+ // DOM elements
335
+ const chatEl = $('#chat');
336
+ const messagesEl = $('#messages');
337
+ const promptEl = $('#prompt');
338
+ const sendBtn = $('#sendBtn');
339
+ const stopBtn = $('#stopBtn');
340
+ const suggestionsEl = $('#suggestions');
341
+ const clearBtn = $('#clearBtn');
342
+ const exportBtn = $('#exportBtn');
343
+
344
+ // State
345
+ let history = [];
346
+ let abortController = null;
347
+
348
+ // Suggestions
349
+ const defaultChips = [
350
+ 'weather in Tokyo', 'time in London', 'define ubiquitous', '2pm PST to CET', '18 celsius to fahrenheit',
351
+ 'who is Ada Lovelace', 'sqrt(144)', 'USD to EUR', 'prime factors of 91'
352
+ ];
353
+
354
+ function renderSuggestions(items=defaultChips){
355
+ suggestionsEl.innerHTML = '';
356
+ items.forEach(text => {
357
+ const chip = document.createElement('button');
358
+ chip.className = 'chip';
359
+ chip.textContent = text;
360
+ chip.onclick = () => {
361
+ promptEl.value = text;
362
+ updateTextareaHeight();
363
+ promptEl.focus();
364
+ };
365
+ suggestionsEl.appendChild(chip);
366
+ });
367
+ }
368
+
369
+ function buildPromptChips(q){
370
+ q = q.trim();
371
+ const chips = [];
372
+ const hasWeather = /\b(weather|temperature)\b/i.test(q);
373
+ const hasDefine = /\b(define|meaning|definition|what does)\b/i.test(q);
374
+ const hasConvert = /\b(to|in|°)\b/i.test(q) || /\b(c|f|cm|inch|kg|lb)\b/.test(q);
375
+ const hasTime = /\b(time|datetime|date)\b/i.test(q);
376
+ const hasWho = /\b(who is|who's|who’re)\b/i.test(q);
377
+ const hasCalc = /[\d\+\-\*\/\^\(\)\.]/.test(q);
378
+
379
+ if (q && !hasWeather) chips.push(q.replace(/\b(what is|what's)\b/i,'').trim() + ' weather');
380
+ if (q && !hasDefine) chips.push('define ' + q.replace(/\b(define|meaning|definition|what does)\b/i,'').trim());
381
+ if (q && !hasConvert && /(?:\d|\b)(°\s?[cf]|celsius|fahrenheit|cm|inch|kg|lb)\b/i.test(q)) chips.push(q + ' to fahrenheit');
382
+ if (q && !hasTime) chips.push('time in ' + q.replace(/\b(what is|what's|time in|date|datetime)\b/ig,'').trim());
383
+ if (q && !hasWho) chips.push('who is ' + q.replace(/\b(who is|who's|who’re)\b/ig,'').trim());
384
+ if (q && !hasCalc) chips.push(q.replace(/\b(calculate|compute|solve)\b/i,'').trim());
385
+
386
+ // de-dup
387
+ const unique = [...new Set(chips)].filter(Boolean).slice(0,6);
388
+ if (unique.length === 0) renderSuggestions(defaultChips);
389
+ else renderSuggestions(unique);
390
+ }
391
+
392
+ // Message helpers
393
+ function addMessage(role, contentHTML, meta = {}){
394
+ const row = document.createElement('div');
395
+ row.className = `row ${role} fade-in`;
396
+
397
+ const avatar = document.createElement('div');
398
+ avatar.className = `avatar ${role}`;
399
+ avatar.textContent = role === 'bot' ? '🤖' : '🙂';
400
+
401
+ const bubble = document.createElement('div');
402
+ bubble.className = `bubble ${role}`;
403
+
404
+ const metaEl = document.createElement('div');
405
+ metaEl.className = 'meta';
406
+ const badge = document.createElement('span');
407
+ badge.className = 'badge';
408
+ badge.textContent = role === 'bot' ? (meta.badge || 'DuckDuckGo') : 'You';
409
+ metaEl.appendChild(badge);
410
+ if (meta.note){
411
+ const note = document.createElement('span');
412
+ note.textContent = meta.note;
413
+ note.style.color = 'var(--muted)';
414
+ metaEl.appendChild(note);
415
+ }
416
+
417
+ const answer = document.createElement('div');
418
+ answer.className = 'answer';
419
+ answer.innerHTML = contentHTML;
420
+
421
+ bubble.appendChild(metaEl);
422
+ bubble.appendChild(answer);
423
+
424
+ if (meta.sources && meta.sources.length){
425
+ const sources = document.createElement('div');
426
+ sources.className = 'sources';
427
+ meta.sources.forEach(src => {
428
+ const s = document.createElement('a');
429
+ s.className = 'source';
430
+ s.href = src.href;
431
+ s.target = '_blank';
432
+ s.rel = 'noopener noreferrer';
433
+ s.textContent = src.label || src.href;
434
+ sources.appendChild(s);
435
+ });
436
+ bubble.appendChild(sources);
437
+ }
438
+
439
+ row.appendChild(avatar);
440
+ row.appendChild(bubble);
441
+ messagesEl.appendChild(row);
442
+ chatEl.scrollTop = chatEl.scrollHeight;
443
+ return bubble;
444
+ }
445
+
446
+ function addTyping(){
447
+ const row = document.createElement('div');
448
+ row.className = 'row bot fade-in';
449
+ row.dataset.typing = '1';
450
+ const avatar = document.createElement('div');
451
+ avatar.className = 'avatar bot';
452
+ avatar.textContent = '🤖';
453
+ const bubble = document.createElement('div');
454
+ bubble.className = 'bubble bot';
455
+ const metaEl = document.createElement('div');
456
+ metaEl.className = 'meta';
457
+ const badge = document.createElement('span');
458
+ badge.className = 'badge';
459
+ badge.textContent = 'DuckDuckGo';
460
+ metaEl.appendChild(badge);
461
+ const answer = document.createElement('div');
462
+ answer.className = 'answer';
463
+ answer.innerHTML = `<span class="typing"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span>`;
464
+ bubble.appendChild(metaEl);
465
+ bubble.appendChild(answer);
466
+ row.appendChild(avatar);
467
+ row.appendChild(bubble);
468
+ messagesEl.appendChild(row);
469
+ chatEl.scrollTop = chatEl.scrollHeight;
470
+ return row;
471
+ }
472
+ function removeTyping(){
473
+ const t = $('[data-typing="1"]', messagesEl);
474
+ if (t) t.remove();
475
+ }
476
+
477
+ // DuckDuckGo fetch
478
+ async function fetchDDG(query){
479
+ const url = new URL('https://api.duckduckgo.com/');
480
+ url.searchParams.set('q', query);
481
+ url.searchParams.set('format', 'json');
482
+ url.searchParams.set('no_html', '1');
483
+ url.searchParams.set('no_redirect', '1');
484
+ // Adding these helps get more structured fields
485
+ url.searchParams.set('skip_disambig', '1');
486
+
487
+ const res = await fetch(url.toString(), {
488
+ method: 'GET',
489
+ headers: {
490
+ 'Accept': 'application/json'
491
+ }
492
+ });
493
+ if (!res.ok) throw new Error('Network error: ' + res.status);
494
+ const data = await res.json();
495
+ return data;
496
+ }
497
+
498
+ // Parse DuckDuckGo result
499
+ function parseDDG(data, originalQuery){
500
+ const sources = [];
501
+ if (Array.isArray(data.Results)) {
502
+ data.Results.forEach(r => {
503
+ if (r && r.FirstURL && r.Text) sources.push({ href: r.FirstURL, label: new URL(r.FirstURL).hostname.replace('www.','') });
504
+ });
505
+ }
506
+ if (data.AbstractURL) sources.push({ href: data.AbstractURL, label: new URL(data.AbstractURL).hostname.replace('www.','') });
507
+ if (data.Answer && data.AnswerURL) sources.push({ href: data.AnswerURL, label: new URL(data.AnswerURL).hostname.replace('www.','') });
508
+
509
+ // Primary text
510
+ let primary = '';
511
+ let note = '';
512
+
513
+ if (data.Answer && data.Answer.trim()){
514
+ primary = data.Answer;
515
+ note = 'Instant Answer';
516
+ } else if (data.Definition && data.Definition.trim()){
517
+ primary = data.Definition;
518
+ note = data.DefinitionSource ? ('Definition • ' + data.DefinitionSource) : 'Definition';
519
+ if (data.DefinitionURL) sources.push({ href: data.DefinitionURL, label: new URL(data.DefinitionURL).hostname.replace('www.','') });
520
+ } else if (data.AbstractText && data.AbstractText.trim()){
521
+ primary = data.AbstractText;
522
+ note = data.Heading ? ('About: ' + data.Heading) : (data.AbstractSource ? data.AbstractSource : 'Abstract');
523
+ } else if (data.RelatedTopics && data.RelatedTopics.length){
524
+ // Build a short list
525
+ const items = [];
526
+ for (const t of data.RelatedTopics){
527
+ if (t && t.Text && t.FirstURL) {
528
+ items.push(`<li><a href="${t.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(t.Text)}</a></li>`);
529
+ } else if (Array.isArray(t.Topics)) {
530
+ for (const tt of t.Topics){
531
+ if (tt && tt.Text && tt.FirstURL) {
532
+ items.push(`<li><a href="${tt.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(tt.Text)}</a></li>`);
533
+ }
534
+ }
535
+ }
536
+ if (items.length >= 6) break;
537
+ }
538
+ if (items.length){
539
+ primary = `<p>Here are some related topics I found:</p><ul>${items.join('')}</ul>`;
540
+ note = 'Related topics';
541
+ }
542
+ }
543
+
544
+ if (!primary){
545
+ // Disambiguation pages often set Heading and nothing else.
546
+ if (data.Heading){
547
+ 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>`;
548
+ } else {
549
+ 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>`;
550
+ }
551
+ note = 'No direct answer';
552
+ }
553
+
554
+ return { html: primary, sources, note, raw: data, query: originalQuery };
555
+ }
556
+
557
+ // Rendering of an answer with small extras for math/units if detected
558
+ function enhanceHTML(html){
559
+ // If the result seems like math (contains equals or numbers with operators), lightly style
560
+ const mathy = /(\d\s*[\+\-\*\/x÷]\s*\d)|(\d+\s*\=\s*\d+)/i.test(stripTags(html));
561
+ if (mathy){
562
+ 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>`;
563
+ }
564
+ return html;
565
+ }
566
+
567
+ // Main send handler
568
+ async function sendMessage(){
569
+ const text = promptEl.value.trim();
570
+ if (!text) return;
571
+
572
+ // Save to history
573
+ history.push({ role: 'user', content: text });
574
+
575
+ // Show user message
576
+ addMessage('user', `<p>${escapeHTML(text)}</p>`);
577
+ promptEl.value = '';
578
+ updateTextareaHeight();
579
+ renderSuggestions(defaultChips);
580
+
581
+ // Prepare typing indicator
582
+ const typingRow = addTyping();
583
+ sendBtn.disabled = true;
584
+ stopBtn.disabled = false;
585
+ abortController = new AbortController();
586
+
587
+ try{
588
+ const data = await fetchDDG(text);
589
+ removeTyping();
590
+ const parsed = parseDDG(data, text);
591
+ addMessage('bot', enhanceHTML(parsed.html), { badge: 'DuckDuckGo', note: parsed.note, sources: parsed.sources });
592
+ history.push({ role: 'assistant', content: parsed.html, sources: parsed.sources });
593
+ }catch(err){
594
+ removeTyping();
595
+ const fallback = `<p>I couldn't reach DuckDuckGo (${escapeHTML(err.message)}). Check your connection and try again.</p>`;
596
+ addMessage('bot', fallback, { badge: 'Error' });
597
+ history.push({ role: 'assistant', content: fallback });
598
+ }finally{
599
+ sendBtn.disabled = false;
600
+ stopBtn.disabled = true;
601
+ abortController = null;
602
+ }
603
+ }
604
+
605
+ // UI events
606
+ function updateTextareaHeight(){
607
+ promptEl.style.height = 'auto';
608
+ const newH = Math.min(promptEl.scrollHeight, 200);
609
+ promptEl.style.height = Math.max(40, newH) + 'px';
610
+ }
611
+ promptEl.addEventListener('input', () => {
612
+ updateTextareaHeight();
613
+ buildPromptChips(promptEl.value);
614
+ });
615
+ promptEl.addEventListener('keydown', (e) => {
616
+ if (e.key === 'Enter' && !e.shiftKey){
617
+ e.preventDefault();
618
+ if (!sendBtn.disabled) sendMessage();
619
+ }
620
+ });
621
+ sendBtn.addEventListener('click', sendMessage);
622
+ stopBtn.addEventListener('click', () => {
623
+ if (abortController){
624
+ abortController.abort();
625
+ removeTyping();
626
+ addMessage('bot', `<p>Request canceled.</p>`, { badge: 'Stopped' });
627
+ stopBtn.disabled = true;
628
+ sendBtn.disabled = false;
629
+ abortController = null;
630
+ }
631
+ });
632
+
633
+ clearBtn.addEventListener('click', () => {
634
+ if (!confirm('Clear the current chat?')) return;
635
+ messagesEl.innerHTML = `
636
+ <div class="row bot fade-in">
637
+ <div class="avatar bot">🤖</div>
638
+ <div class="bubble bot">
639
+ <div class="meta"><span class="badge">Bot</span><span>DuckDuckGo Instant Answers</span></div>
640
+ <div class="answer"><p>Chat cleared. Ask me something new!</p></div>
641
+ </div>
642
+ </div>`;
643
+ history = [];
644
+ renderSuggestions(defaultChips);
645
+ });
646
+
647
+ exportBtn.addEventListener('click', () => {
648
+ const payload = {
649
+ exportedAt: new Date().toISOString(),
650
+ history
651
+ };
652
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
653
+ const url = URL.createObjectURL(blob);
654
+ const a = document.createElement('a');
655
+ a.href = url;
656
+ a.download = 'ddg-chat-export.json';
657
+ document.body.appendChild(a);
658
+ a.click();
659
+ URL.revokeObjectURL(url);
660
+ a.remove();
661
+ });
662
+
663
+ // Initial
664
+ renderSuggestions(defaultChips);
665
+ updateTextareaHeight();
666
+
667
+ // Friendly: prefill a sample on first load if empty
668
+ setTimeout(() => {
669
+ if (!promptEl.value.trim()) {
670
+ promptEl.value = 'weather in Paris';
671
+ updateTextareaHeight();
672
+ buildPromptChips(promptEl.value);
673
+ }
674
+ }, 300);
675
+ </script>
676
+ </body>
677
+ </html>