Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>DuckDuckGo AI Chatbot</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <meta name="description" content="A simple chatbot that answers using DuckDuckGo instant answers." /> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0b1020; | |
| --bg2: #0f1630; | |
| --panel: #0f1732; | |
| --panel-2: #111a3b; | |
| --text: #e7ecff; | |
| --muted: #a3b1d9; | |
| --accent: #6ea8fe; | |
| --accent-2: #8ad1ff; | |
| --success: #4ade80; | |
| --warning: #facc15; | |
| --danger: #f87171; | |
| --bubble-user: #1a254f; | |
| --bubble-bot: #141e3d; | |
| --shadow: 0 10px 30px rgba(0, 0, 0, .35); | |
| --radius: 14px; | |
| } | |
| * { | |
| box-sizing: border-box | |
| } | |
| html, | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| height: 100%; | |
| font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; | |
| color: var(--text); | |
| background: | |
| radial-gradient(1200px 800px at 10% -20%, #20306a 0%, transparent 60%), | |
| radial-gradient(900px 700px at 110% 10%, #1b3c7a 0%, transparent 60%), | |
| radial-gradient(700px 900px at 50% 120%, #1a365d 0%, transparent 60%), | |
| linear-gradient(180deg, var(--bg), var(--bg2)); | |
| background-attachment: fixed; | |
| } | |
| a { | |
| color: var(--accent) | |
| } | |
| .app { | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100%; | |
| width: 100%; | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| backdrop-filter: saturate(180%) blur(8px); | |
| background: linear-gradient(180deg, rgba(10, 16, 34, .8), rgba(10, 16, 34, .35)); | |
| border-bottom: 1px solid rgba(255, 255, 255, .06); | |
| } | |
| .topbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 14px 18px; | |
| gap: 12px; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| min-width: 0; | |
| } | |
| .logo { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 10px; | |
| background: conic-gradient(from 210deg, #6ea8fe, #8ad1ff, #6ea8fe); | |
| box-shadow: 0 6px 16px rgba(110, 168, 254, .35), inset 0 0 18px rgba(255, 255, 255, .2); | |
| position: relative; | |
| } | |
| .logo::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 3px; | |
| border-radius: 8px; | |
| background: radial-gradient(120px 80px at 30% 20%, rgba(255, 255, 255, .5), transparent 40%), | |
| linear-gradient(180deg, rgba(255, 255, 255, .12), rgba(0, 0, 0, .18)); | |
| } | |
| .titles { | |
| min-width: 0; | |
| } | |
| .title { | |
| font-weight: 800; | |
| letter-spacing: .2px; | |
| font-size: 18px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .subtitle { | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| .links { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .btn { | |
| appearance: none; | |
| border: none; | |
| cursor: pointer; | |
| font: inherit; | |
| color: inherit; | |
| padding: 10px 14px; | |
| border-radius: 10px; | |
| background: #1a254f; | |
| color: #dfe8ff; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| border: 1px solid rgba(255, 255, 255, .08); | |
| transition: transform .08s ease, background .2s ease, border-color .2s ease, opacity .2s ease; | |
| } | |
| .btn:hover { | |
| transform: translateY(-1px); | |
| border-color: rgba(255, 255, 255, .18) | |
| } | |
| .btn.secondary { | |
| background: #0f1732; | |
| } | |
| .btn.ghost { | |
| background: transparent; | |
| border-color: rgba(255, 255, 255, .1) | |
| } | |
| .btn:disabled { | |
| opacity: .6; | |
| cursor: not-allowed; | |
| transform: none | |
| } | |
| .icon { | |
| width: 18px; | |
| height: 18px; | |
| display: inline-block; | |
| } | |
| main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| padding: 12px; | |
| } | |
| .chat { | |
| flex: 1; | |
| overflow: auto; | |
| padding: 12px; | |
| padding-bottom: 24px; | |
| scroll-behavior: smooth; | |
| } | |
| .messages { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .row { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| } | |
| .row.user { | |
| justify-content: flex-end; | |
| } | |
| .row.bot { | |
| justify-content: flex-start; | |
| } | |
| .avatar { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| background: #172046; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| font-weight: 700; | |
| border: 1px solid rgba(255, 255, 255, .08); | |
| box-shadow: var(--shadow); | |
| flex-shrink: 0; | |
| } | |
| .avatar.bot { | |
| background: radial-gradient(100px 80px at 20% 10%, #2a3d7a, #192555); | |
| } | |
| .avatar.user { | |
| background: radial-gradient(120px 90px at 30% 10%, #1a2c5f, #0e1736); | |
| } | |
| .bubble { | |
| max-width: min(800px, 86vw); | |
| padding: 12px 14px; | |
| border-radius: var(--radius); | |
| border: 1px solid rgba(255, 255, 255, .08); | |
| box-shadow: var(--shadow); | |
| line-height: 1.55; | |
| position: relative; | |
| word-wrap: break-word; | |
| overflow-wrap: anywhere; | |
| } | |
| .bubble.user { | |
| background: linear-gradient(180deg, #152152, #101a3e); | |
| border-top-right-radius: 6px; | |
| } | |
| .bubble.bot { | |
| background: linear-gradient(180deg, #111a3b, #0c1431); | |
| border-bottom-left-radius: 6px; | |
| } | |
| .bubble .meta { | |
| font-size: 12px; | |
| color: var(--muted); | |
| margin-bottom: 6px; | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .badge { | |
| padding: 2px 8px; | |
| border-radius: 999px; | |
| background: #0c1330; | |
| border: 1px solid rgba(255, 255, 255, .08); | |
| font-size: 11px; | |
| color: #c7d5ff; | |
| } | |
| .answer { | |
| font-size: 15px; | |
| color: #e9efff; | |
| } | |
| .answer p { | |
| margin: 0 0 10px 0; | |
| } | |
| .answer ul, | |
| .answer ol { | |
| margin: 6px 0 10px 18px; | |
| } | |
| .answer a { | |
| color: var(--accent-2); | |
| text-decoration: underline; | |
| } | |
| .sources { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-top: 8px; | |
| } | |
| .source { | |
| font-size: 12px; | |
| color: #c7d5ff; | |
| background: #0c1330; | |
| border: 1px solid rgba(255, 255, 255, .08); | |
| padding: 4px 8px; | |
| border-radius: 8px; | |
| } | |
| .composer { | |
| position: sticky; | |
| bottom: 0; | |
| z-index: 9; | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| padding: 12px; | |
| background: linear-gradient(180deg, rgba(10, 16, 34, .0), rgba(10, 16, 34, .85)); | |
| backdrop-filter: blur(8px); | |
| border-top: 1px solid rgba(255, 255, 255, .06); | |
| } | |
| .input-wrap { | |
| flex: 1; | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| background: linear-gradient(180deg, #0e1633, #0b122b); | |
| border: 1px solid rgba(255, 255, 255, .08); | |
| border-radius: 14px; | |
| padding: 8px; | |
| box-shadow: var(--shadow); | |
| } | |
| textarea { | |
| flex: 1; | |
| resize: none; | |
| border: none; | |
| outline: none; | |
| background: transparent; | |
| color: var(--text); | |
| font: inherit; | |
| line-height: 1.4; | |
| max-height: 200px; | |
| min-height: 40px; | |
| padding: 8px 10px; | |
| } | |
| .actions { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .small-btn { | |
| width: 38px; | |
| height: 38px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(255, 255, 255, .1); | |
| background: #0e1633; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: transform .08s ease, border-color .2s ease, background .2s ease; | |
| } | |
| .small-btn:hover { | |
| transform: translateY(-1px); | |
| border-color: rgba(255, 255, 255, .2) | |
| } | |
| .small-btn:disabled { | |
| opacity: .6; | |
| cursor: not-allowed; | |
| transform: none | |
| } | |
| .suggestions { | |
| position: absolute; | |
| left: 12px; | |
| right: 12px; | |
| bottom: 64px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| pointer-events: none; | |
| } | |
| .chip { | |
| pointer-events: auto; | |
| background: #0d1431; | |
| border: 1px solid rgba(255, 255, 255, .1); | |
| color: #d8e4ff; | |
| padding: 6px 10px; | |
| border-radius: 999px; | |
| font-size: 12px; | |
| cursor: pointer; | |
| transition: transform .08s ease, background .2s ease, border-color .2s ease; | |
| } | |
| .chip:hover { | |
| transform: translateY(-1px); | |
| background: #121c44; | |
| border-color: rgba(255, 255, 255, .2) | |
| } | |
| .typing { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .dot { | |
| width: 6px; | |
| height: 6px; | |
| background: #c7d5ff; | |
| border-radius: 50%; | |
| opacity: .8; | |
| animation: blink 1.4s infinite ease-in-out; | |
| } | |
| .dot:nth-child(2) { | |
| animation-delay: .2s | |
| } | |
| .dot:nth-child(3) { | |
| animation-delay: .4s | |
| } | |
| @keyframes blink { | |
| 0%, | |
| 80%, | |
| 100% { | |
| transform: translateY(0); | |
| opacity: .5 | |
| } | |
| 40% { | |
| transform: translateY(-3px); | |
| opacity: 1 | |
| } | |
| } | |
| .hint { | |
| font-size: 12px; | |
| color: var(--muted); | |
| padding: 4px 2px 0 2px; | |
| } | |
| .footer-note { | |
| text-align: center; | |
| font-size: 12px; | |
| color: var(--muted); | |
| padding: 10px 0 18px; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 720px) { | |
| .topbar { | |
| padding: 12px | |
| } | |
| .titles .title { | |
| font-size: 17px | |
| } | |
| .links { | |
| display: none | |
| } | |
| .bubble { | |
| max-width: min(760px, 92vw) | |
| } | |
| .suggestions { | |
| bottom: 60px | |
| } | |
| } | |
| /* Scrollbar */ | |
| .chat::-webkit-scrollbar { | |
| width: 10px | |
| } | |
| .chat::-webkit-scrollbar-thumb { | |
| background: #16214a; | |
| border-radius: 10px; | |
| border: 2px solid transparent; | |
| background-clip: padding-box; | |
| } | |
| .chat::-webkit-scrollbar-track { | |
| background: transparent | |
| } | |
| /* Simple fade-in for messages */ | |
| .fade-in { | |
| animation: fadeIn .22s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(6px) | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0) | |
| } | |
| } | |
| .mono { | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | |
| font-size: 12px; | |
| background: #0b1434; | |
| border: 1px solid rgba(255, 255, 255, .08); | |
| border-radius: 8px; | |
| padding: 10px; | |
| overflow: auto; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <header> | |
| <div class="topbar"> | |
| <div class="brand"> | |
| <div class="logo" aria-hidden="true"></div> | |
| <div class="titles"> | |
| <div class="title">DuckDuckGo Answer Bot</div> | |
| <div class="subtitle">Real-time answers via DuckDuckGo Instant Answers API (CORS-proxied).</div> | |
| </div> | |
| </div> | |
| <div class="links"> | |
| <a class="btn ghost" href="https://duckduckgo.com/" target="_blank" rel="noopener noreferrer"> | |
| <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> | |
| DuckDuckGo | |
| </a> | |
| <button id="exportBtn" class="btn secondary" title="Export chat as JSON"> | |
| <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> | |
| Export | |
| </button> | |
| <button id="clearBtn" class="btn" title="Clear chat"> | |
| <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> | |
| Clear | |
| </button> | |
| <a class="btn" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener noreferrer" | |
| title="Built with anycoder"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="chat" id="chat"> | |
| <div class="messages" id="messages"> | |
| <div class="row bot fade-in"> | |
| <div class="avatar bot">🤖</div> | |
| <div class="bubble bot"> | |
| <div class="meta"> | |
| <span class="badge">Bot</span> | |
| <span>DuckDuckGo Instant Answers</span> | |
| </div> | |
| <div class="answer"> | |
| <p>Hi! I’m your DuckDuckGo-powered assistant. Ask me anything: facts, definitions, unit conversions, | |
| weather, time, math, and more.</p> | |
| <p class="hint">Tip: Try queries like “weather in Tokyo”, “define ubiquitous”, “2pm PST to CET”, or | |
| “what is 18 celsius in fahrenheit”.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="suggestions" id="suggestions"></div> | |
| <div class="composer"> | |
| <div class="input-wrap"> | |
| <textarea id="prompt" rows="1" placeholder="Ask me anything... (Shift+Enter = newline)"></textarea> | |
| <div class="actions"> | |
| <button class="small-btn" id="stopBtn" disabled title="Stop"> | |
| <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> | |
| </button> | |
| <button class="small-btn" id="sendBtn" title="Send (Enter)"> | |
| <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> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <div class="footer-note"> | |
| Answers come from DuckDuckGo Instant Answers. CORS is bypassed via public proxies for in-browser usage. | |
| </div> | |
| </div> | |
| <script> | |
| // Utilities | |
| const $ = (sel, el=document) => el.querySelector(sel); | |
| const $$ = (sel, el=document) => [...el.querySelectorAll(sel)]; | |
| const escapeHTML = (s) => s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); | |
| const stripTags = (s) => s.replace(/<[^>]*>/g, ''); | |
| const truncate = (s, n=220) => s.length > n ? s.slice(0, n-1) + '…' : s; | |
| // DOM elements | |
| const chatEl = $('#chat'); | |
| const messagesEl = $('#messages'); | |
| const promptEl = $('#prompt'); | |
| const sendBtn = $('#sendBtn'); | |
| const stopBtn = $('#stopBtn'); | |
| const suggestionsEl = $('#suggestions'); | |
| const clearBtn = $('#clearBtn'); | |
| const exportBtn = $('#exportBtn'); | |
| // State | |
| let history = []; | |
| let abortController = null; | |
| // Suggestions | |
| const defaultChips = [ | |
| 'weather in Tokyo', 'time in London', 'define ubiquitous', '2pm PST to CET', '18 celsius to fahrenheit', | |
| 'who is Ada Lovelace', 'sqrt(144)', 'USD to EUR', 'prime factors of 91' | |
| ]; | |
| function renderSuggestions(items=defaultChips){ | |
| suggestionsEl.innerHTML = ''; | |
| items.forEach(text => { | |
| const chip = document.createElement('button'); | |
| chip.className = 'chip'; | |
| chip.textContent = text; | |
| chip.onclick = () => { | |
| promptEl.value = text; | |
| updateTextareaHeight(); | |
| promptEl.focus(); | |
| }; | |
| suggestionsEl.appendChild(chip); | |
| }); | |
| } | |
| function buildPromptChips(q){ | |
| q = q.trim(); | |
| const chips = []; | |
| const hasWeather = /\b(weather|temperature)\b/i.test(q); | |
| const hasDefine = /\b(define|meaning|definition|what does)\b/i.test(q); | |
| const hasConvert = /\b(to|in|°)\b/i.test(q) || /\b(c|f|cm|inch|kg|lb)\b/.test(q); | |
| const hasTime = /\b(time|datetime|date)\b/i.test(q); | |
| const hasWho = /\b(who is|who's|who’re)\b/i.test(q); | |
| const hasCalc = /[\d\+\-\*\/\^\(\)\.]/.test(q); | |
| if (q && !hasWeather) chips.push(q.replace(/\b(what is|what's)\b/i,'').trim() + ' weather'); | |
| if (q && !hasDefine) chips.push('define ' + q.replace(/\b(define|meaning|definition|what does)\b/i,'').trim()); | |
| if (q && !hasConvert && /(?:\d|\b)(°\s?[cf]|celsius|fahrenheit|cm|inch|kg|lb)\b/i.test(q)) chips.push(q + ' to fahrenheit'); | |
| if (q && !hasTime) chips.push('time in ' + q.replace(/\b(what is|what's|time in|date|datetime)\b/ig,'').trim()); | |
| if (q && !hasWho) chips.push('who is ' + q.replace(/\b(who is|who's|who’re)\b/ig,'').trim()); | |
| if (q && !hasCalc) chips.push(q.replace(/\b(calculate|compute|solve)\b/i,'').trim()); | |
| // de-dup | |
| const unique = [...new Set(chips)].filter(Boolean).slice(0,6); | |
| if (unique.length === 0) renderSuggestions(defaultChips); | |
| else renderSuggestions(unique); | |
| } | |
| // Message helpers | |
| function addMessage(role, contentHTML, meta = {}){ | |
| const row = document.createElement('div'); | |
| row.className = `row ${role} fade-in`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = `avatar ${role}`; | |
| avatar.textContent = role === 'bot' ? '🤖' : '🙂'; | |
| const bubble = document.createElement('div'); | |
| bubble.className = `bubble ${role}`; | |
| const metaEl = document.createElement('div'); | |
| metaEl.className = 'meta'; | |
| const badge = document.createElement('span'); | |
| badge.className = 'badge'; | |
| badge.textContent = role === 'bot' ? (meta.badge || 'DuckDuckGo') : 'You'; | |
| metaEl.appendChild(badge); | |
| if (meta.note){ | |
| const note = document.createElement('span'); | |
| note.textContent = meta.note; | |
| note.style.color = 'var(--muted)'; | |
| metaEl.appendChild(note); | |
| } | |
| const answer = document.createElement('div'); | |
| answer.className = 'answer'; | |
| answer.innerHTML = contentHTML; | |
| bubble.appendChild(metaEl); | |
| bubble.appendChild(answer); | |
| if (meta.sources && meta.sources.length){ | |
| const sources = document.createElement('div'); | |
| sources.className = 'sources'; | |
| meta.sources.forEach(src => { | |
| const s = document.createElement('a'); | |
| s.className = 'source'; | |
| s.href = src.href; | |
| s.target = '_blank'; | |
| s.rel = 'noopener noreferrer'; | |
| s.textContent = src.label || src.href; | |
| sources.appendChild(s); | |
| }); | |
| bubble.appendChild(sources); | |
| } | |
| row.appendChild(avatar); | |
| row.appendChild(bubble); | |
| messagesEl.appendChild(row); | |
| chatEl.scrollTop = chatEl.scrollHeight; // Auto scroll down | |
| return bubble; | |
| } | |
| function addTyping(){ | |
| const row = document.createElement('div'); | |
| row.className = 'row bot fade-in'; | |
| row.dataset.typing = '1'; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'avatar bot'; | |
| avatar.textContent = '🤖'; | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'bubble bot'; | |
| const metaEl = document.createElement('div'); | |
| metaEl.className = 'meta'; | |
| const badge = document.createElement('span'); | |
| badge.className = 'badge'; | |
| badge.textContent = 'DuckDuckGo'; | |
| metaEl.appendChild(badge); | |
| const answer = document.createElement('div'); | |
| answer.className = 'answer'; | |
| answer.innerHTML = `<span class="typing"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span>`; | |
| bubble.appendChild(metaEl); | |
| bubble.appendChild(answer); | |
| row.appendChild(avatar); | |
| row.appendChild(bubble); | |
| messagesEl.appendChild(row); | |
| chatEl.scrollTop = chatEl.scrollHeight; // Auto scroll down | |
| return row; | |
| } | |
| function removeTyping(){ | |
| const t = $('[data-typing="1"]', messagesEl); | |
| if (t) t.remove(); | |
| } | |
| // Build DuckDuckGo API URL | |
| function 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(); | |
| } | |
| // CORS proxy list (tried in order). Using public proxies to bypass CORS in-browser. | |
| const CORS_PROXIES = [ | |
| // https://cors.isomorphic-git.org/ simply adds permissive CORS headers to any URL | |
| (target) => `https://cors.isomorphic-git.org/${target}`, | |
| // https://api.codetabs.com/v1/proxy?quest= forwards the request server-side | |
| (target) => `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(target)}`, | |
| // https://corsproxy.io/? provides a minimal CORS proxy | |
| (target) => `https://corsproxy.io/?${encodeURIComponent(target)}` | |
| ]; | |
| // Fetch with automatic CORS proxy fallback (real DuckDuckGo data only). | |
| async function fetchWithCorsProxies(targetUrl, signal){ | |
| let lastErr = null; | |
| for (const buildProxy of CORS_PROXIES){ | |
| const proxyUrl = buildProxy(targetUrl); | |
| try{ | |
| const res = await fetch(proxyUrl, { signal, headers: { 'Accept': 'application/json' } }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const contentType = res.headers.get('content-type') || ''; | |
| // Some proxies may return text; we only accept JSON | |
| if (!contentType.includes('application/json') && !contentType.includes('text/plain')) { | |
| throw new Error('Unexpected content-type: ' + contentType); | |
| } | |
| // If it's text (e.g., JSON as text), still parse it | |
| const text = await res.text(); | |
| const data = JSON.parse(text); | |
| return data; | |
| }catch(err){ | |
| lastErr = err; | |
| // try next proxy | |
| } | |
| } | |
| throw lastErr || new Error('All CORS proxies failed'); | |
| } | |
| // DuckDuckGo fetch (real data only, via CORS proxies) | |
| async function fetchDDG(query, signal){ | |
| const targetUrl = buildDDGUrl(query); | |
| const data = await fetchWithCorsProxies(targetUrl, signal); | |
| return data; | |
| } | |
| // Parse DuckDuckGo result | |
| function parseDDG(data, originalQuery){ | |
| const sources = []; | |
| if (Array.isArray(data.Results)) { | |
| data.Results.forEach(r => { | |
| if (r && r.FirstURL && r.Text) sources.push({ href: r.FirstURL, label: new URL(r.FirstURL).hostname.replace('www.','') }); | |
| }); | |
| } | |
| if (data.AbstractURL) sources.push({ href: data.AbstractURL, label: new URL(data.AbstractURL).hostname.replace('www.','') }); | |
| if (data.Answer && data.AnswerURL) sources.push({ href: data.AnswerURL, label: new URL(data.AnswerURL).hostname.replace('www.','') }); | |
| // Primary text | |
| let primary = ''; | |
| let note = ''; | |
| if (data.Answer && data.Answer.trim()){ | |
| primary = data.Answer; | |
| note = 'Instant Answer'; | |
| } 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.','') }); | |
| } else if (data.AbstractText && data.AbstractText.trim()){ | |
| primary = data.AbstractText; | |
| note = data.Heading ? ('About: ' + data.Heading) : (data.AbstractSource ? data.AbstractSource : 'Abstract'); | |
| } else if (data.RelatedTopics && data.RelatedTopics.length){ | |
| // Build a short list | |
| const items = []; | |
| for (const t of data.RelatedTopics){ | |
| if (t && t.Text && t.FirstURL) { | |
| items.push(`<li><a href="${t.FirstURL}" target="_blank" rel="noopener noreferrer">${escapeHTML(t.Text)}</a></li>`); | |
| } else if (Array.isArray(t.Topics)) { | |
| 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>`); | |
| } | |
| } | |
| } | |
| if (items.length >= 6) break; | |
| } | |
| if (items.length){ | |
| primary = `<p>Here are some related topics I found:</p><ul>${items.join('')}</ul>`; | |
| note = 'Related topics'; | |
| } | |
| } | |
| if (!primary){ | |
| // Disambiguation pages often set Heading and nothing else. | |
| if (data.Heading){ | |
| 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>`; | |
| } else { | |
| 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>`; | |
| } | |
| note = 'No direct answer'; | |
| } | |
| return { html: primary, sources, note, raw: data, query: originalQuery }; | |
| } | |
| // Rendering of an answer with small extras for math/units if detected | |
| function enhanceHTML(html){ | |
| // If the result seems like math (contains equals or numbers with operators), lightly style | |
| const mathy = /(\d\s*[\+\-\*\/x÷]\s*\d)|(\d+\s*\=\s*\d+)/i.test(stripTags(html)); | |
| if (mathy){ | |
| 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>`; | |
| } | |
| return html; | |
| } | |
| // Main send handler | |
| async function sendMessage(){ | |
| const text = promptEl.value.trim(); | |
| if (!text) return; | |
| // Save to history | |
| history.push({ role: 'user', content: text }); | |
| // Show user message | |
| addMessage('user', `<p>${escapeHTML(text)}</p>`); | |
| promptEl.value = ''; | |
| updateTextareaHeight(); | |
| renderSuggestions(defaultChips); | |
| // Prepare typing indicator | |
| const typingRow = addTyping(); | |
| sendBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| abortController = new AbortController(); | |
| try{ | |
| const data = await fetchDDG(text, abortController.signal); | |
| removeTyping(); | |
| const parsed = parseDDG(data, text); | |
| addMessage('bot', enhanceHTML(parsed.html), { badge: 'DuckDuckGo', note: parsed.note, sources: parsed.sources }); | |
| history.push({ role: 'assistant', content: parsed.html, sources: parsed.sources }); | |
| }catch(err){ | |
| removeTyping(); | |
| const fallback = `<p>Network error or CORS proxy issue: ${escapeHTML(err.message)}. Please try again.</p>`; | |
| addMessage('bot', fallback, { badge: 'Error' }); | |
| history.push({ role: 'assistant', content: fallback }); | |
| }finally{ | |
| sendBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| abortController = null; | |
| } | |
| } | |
| // UI events | |
| function updateTextareaHeight(){ | |
| promptEl.style.height = 'auto'; | |
| const newH = Math.min(promptEl.scrollHeight, 200); | |
| promptEl.style.height = Math.max(40, newH) + 'px'; | |
| } | |
| promptEl.addEventListener('input', () => { | |
| updateTextareaHeight(); | |
| buildPromptChips(promptEl.value); | |
| }); | |
| promptEl.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey){ | |
| e.preventDefault(); | |
| if (!sendBtn.disabled) sendMessage(); | |
| } | |
| }); | |
| sendBtn.addEventListener('click', sendMessage); | |
| stopBtn.addEventListener('click', () => { | |
| if (abortController){ | |
| abortController.abort(); | |
| removeTyping(); | |
| addMessage('bot', `<p>Request canceled.</p>`, { badge: 'Stopped' }); | |
| stopBtn.disabled = true; | |
| sendBtn.disabled = false; | |
| abortController = null; | |
| } | |
| }); | |
| clearBtn.addEventListener('click', () => { | |
| if (!confirm('Clear the current chat?')) return; | |
| messagesEl.innerHTML = ` | |
| <div class="row bot fade-in"> | |
| <div class="avatar bot">🤖</div> | |
| <div class="bubble bot"> | |
| <div class="meta"><span class="badge">Bot</span><span>DuckDuckGo Instant Answers</span></div> | |
| <div class="answer"><p>Chat cleared. Ask me something new!</p></div> | |
| </div> | |
| </div>`; | |
| history = []; | |
| renderSuggestions(defaultChips); | |
| }); | |
| exportBtn.addEventListener('click', () => { | |
| const payload = { | |
| exportedAt: new Date().toISOString(), | |
| history | |
| }; | |
| const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'ddg-chat-export.json'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| a.remove(); | |
| }); | |
| // Initial | |
| renderSuggestions(defaultChips); | |
| updateTextareaHeight(); | |
| </script> | |
| </body> | |
| </html> |