anycoder-bba383d3 / index.html
AiCoderv2's picture
Upload folder using huggingface_hub
4261cb3 verified
raw
history blame
29.6 kB
<!DOCTYPE html>
<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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>