Spaces:
Running
Running
| /** | |
| * AI Web Search Assistant | |
| * Uses DuckDuckGo Instant Answer API for real-time responses | |
| */ | |
| // Configuration | |
| const CONFIG = { | |
| DDG_API_BASE: 'https://api.duckduckgo.com/', | |
| MAX_TOPICS: 8, | |
| // Use a CORS proxy for better compatibility | |
| CORS_PROXY: 'https://corsproxy.io/?' | |
| }; | |
| // DOM Elements | |
| const elements = { | |
| chatMessages: document.getElementById('chatMessages'), | |
| chatForm: document.getElementById('chatForm'), | |
| userInput: document.getElementById('userInput'), | |
| sendButton: document.getElementById('sendButton'), | |
| statusBar: document.getElementById('statusBar'), | |
| statusText: document.getElementById('statusText'), | |
| loadingTemplate: document.getElementById('loadingTemplate') | |
| }; | |
| // State | |
| let isLoading = false; | |
| /** | |
| * Update status bar | |
| */ | |
| function updateStatus(text, state = 'ready') { | |
| elements.statusText.textContent = text; | |
| elements.statusBar.className = 'status-bar'; | |
| if (state === 'searching') { | |
| elements.statusBar.classList.add('searching'); | |
| } else if (state === 'error') { | |
| elements.statusBar.classList.add('error'); | |
| } | |
| } | |
| /** | |
| * Create a loading message element | |
| */ | |
| function createLoadingMessage() { | |
| const template = elements.loadingTemplate.content.cloneNode(true); | |
| return template.querySelector('.loading-message'); | |
| } | |
| /** | |
| * Add a message to the chat | |
| */ | |
| function addMessage(content, isUser = false) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.innerHTML = isUser | |
| ? '<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="8" r="4" fill="white"/><path d="M4 20C4 17.2386 7.58172 15 12 15C16.4183 15 20 17.2386 20 20" stroke="white" stroke-width="2"/></svg>' | |
| : '<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M12 2L2 7L12 12L22 7L12 2Z" fill="#4F46E5"/></svg>'; | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'message-content'; | |
| if (typeof content === 'string') { | |
| contentDiv.innerHTML = `<p>${content}</p>`; | |
| } else { | |
| contentDiv.appendChild(content); | |
| } | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(contentDiv); | |
| elements.chatMessages.appendChild(messageDiv); | |
| // Scroll to bottom | |
| elements.chatMessages.parentElement.scrollTop = elements.chatMessages.parentElement.scrollHeight; | |
| return messageDiv; | |
| } | |
| /** | |
| * Create an answer section | |
| */ | |
| function createAnswerSection(title, content, className = '', source = null) { | |
| const section = document.createElement('div'); | |
| section.className = `answer-section ${className}`; | |
| let html = `<h4>${title}</h4><p>${content}</p>`; | |
| if (source) { | |
| html += `<p class="source-link">Source: ${source}</p>`; | |
| } | |
| section.innerHTML = html; | |
| return section; | |
| } | |
| /** | |
| * Format DuckDuckGo response for display | |
| */ | |
| function formatDuckDuckGoResponse(data) { | |
| const container = document.createElement('div'); | |
| let hasContent = false; | |
| // Debug: Log the raw response | |
| console.log('DuckDuckGo Response:', data); | |
| // Check for Instant Answer (most prominent) | |
| if (data.Answer && data.Answer.trim()) { | |
| const answerSection = createAnswerSection('π¬ Answer', data.Answer, 'answer'); | |
| if (data.AnswerType) { | |
| answerSection.querySelector('h4').textContent += ` (${data.AnswerType})`; | |
| } | |
| container.appendChild(answerSection); | |
| hasContent = true; | |
| } | |
| // Check for Definition | |
| if (data.Definition && data.Definition.trim()) { | |
| const defSection = createAnswerSection('π Definition', data.Definition, 'definition', data.DefinitionSource); | |
| container.appendChild(defSection); | |
| hasContent = true; | |
| } | |
| // Check for Abstract (main topic info) | |
| if (data.Abstract && data.Abstract.trim()) { | |
| const abstractSection = createAnswerSection( | |
| `π ${data.AbstractTitle || 'Topic Overview'}`, | |
| data.Abstract, | |
| 'abstract', | |
| data.AbstractSource | |
| ); | |
| container.appendChild(abstractSection); | |
| hasContent = true; | |
| } | |
| // Check for Related Topics (from main entity) | |
| if (data.RelatedTopics && data.RelatedTopics.length > 0) { | |
| const topicsSection = document.createElement('div'); | |
| topicsSection.className = 'related-topics'; | |
| const title = document.createElement('h4'); | |
| title.textContent = 'π Related Topics'; | |
| topicsSection.appendChild(title); | |
| const tagsContainer = document.createElement('div'); | |
| tagsContainer.className = 'topic-tags'; | |
| data.RelatedTopics.slice(0, CONFIG.MAX_TOPICS).forEach(topic => { | |
| if (topic.Text && topic.FirstURL) { | |
| const tag = document.createElement('button'); | |
| tag.className = 'topic-tag'; | |
| tag.textContent = topic.Text.replace(/<[^>]*>/g, ''); | |
| tag.addEventListener('click', () => { | |
| elements.userInput.value = topic.Text.replace(/<[^>]*>/g, ''); | |
| elements.chatForm.dispatchEvent(new Event('submit')); | |
| }); | |
| tagsContainer.appendChild(tag); | |
| } else if (topic.Name && topic.Topics) { | |
| // Grouped topics (e.g., for famous people) | |
| topic.Topics.slice(0, 3).forEach(subTopic => { | |
| if (subTopic.Text && subTopic.FirstURL) { | |
| const tag = document.createElement('button'); | |
| tag.className = 'topic-tag'; | |
| tag.textContent = subTopic.Text.replace(/<[^>]*>/g, ''); | |
| tag.addEventListener('click', () => { | |
| elements.userInput.value = subTopic.Text.replace(/<[^>]*>/g, ''); | |
| elements.chatForm.dispatchEvent(new Event('submit')); | |
| }); | |
| tagsContainer.appendChild(tag); | |
| } | |
| }); | |
| } | |
| }); | |
| if (tagsContainer.children.length > 0) { | |
| topicsSection.appendChild(tagsContainer); | |
| container.appendChild(topicsSection); | |
| hasContent = true; | |
| } | |
| } | |
| // Check for Results (additional web results) | |
| if (data.Results && data.Results.length > 0) { | |
| const resultsSection = document.createElement('div'); | |
| resultsSection.className = 'related-topics'; | |
| const title = document.createElement('h4'); | |
| title.textContent = 'π More Results'; | |
| resultsSection.appendChild(title); | |
| const tagsContainer = document.createElement('div'); | |
| tagsContainer.className = 'topic-tags'; | |
| data.Results.slice(0, CONFIG.MAX_TOPICS).forEach(result => { | |
| if (result.Text && result.FirstURL) { | |
| const tag = document.createElement('button'); | |
| tag.className = 'topic-tag'; | |
| tag.textContent = result.Text.replace(/<[^>]*>/g, '').substring(0, 50); | |
| tag.title = result.FirstURL; | |
| tag.addEventListener('click', () => { | |
| window.open(result.FirstURL, '_blank'); | |
| }); | |
| tagsContainer.appendChild(tag); | |
| } | |
| }); | |
| if (tagsContainer.children.length > 0) { | |
| resultsSection.appendChild(tagsContainer); | |
| container.appendChild(resultsSection); | |
| hasContent = true; | |
| } | |
| } | |
| // Check for Redirect (when query needs different search) | |
| if (data.Redirect && data.Redirect.trim()) { | |
| const redirectMsg = document.createElement('p'); | |
| redirectMsg.innerHTML = `π For better results, try searching for: <strong>${data.Redirect}</strong>`; | |
| container.appendChild(redirectMsg); | |
| hasContent = true; | |
| } | |
| // If no data found - provide helpful message | |
| if (!hasContent) { | |
| const noResult = document.createElement('div'); | |
| noResult.innerHTML = ` | |
| <p>π I couldn't find specific information for that query.</p> | |
| <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--text-secondary);"> | |
| Try:</p> | |
| <ul style="margin-left: 1.25rem; margin-top: 0.25rem; color: var(--text-secondary); font-size: 0.875rem;"> | |
| <li>Using different keywords</li> | |
| <li>Asking a factual question</li> | |
| <li>Searching for a specific person, place, or concept</li> | |
| </ul> | |
| `; | |
| container.appendChild(noResult); | |
| } | |
| return container; | |
| } | |
| /** | |
| * Fetch answer from DuckDuckGo API | |
| */ | |
| async function fetchDuckDuckGoAnswer(query) { | |
| const params = new URLSearchParams({ | |
| q: query, | |
| format: 'json', | |
| no_html: '1', | |
| skip_disambig: '1', | |
| pretty: '1' | |
| }); | |
| const url = `${CONFIG.DDG_API_BASE}?${params}`; | |
| // Try direct first, then with CORS proxy | |
| const urlsToTry = [url, CONFIG.CORS_PROXY + encodeURIComponent(url)]; | |
| let lastError = null; | |
| for (const attemptUrl of urlsToTry) { | |
| try { | |
| console.log('Trying URL:', attemptUrl); | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 10000); | |
| const response = await fetch(attemptUrl, { | |
| method: 'GET', | |
| headers: { | |
| 'Accept': 'application/json', | |
| }, | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeoutId); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| // Check if we got any meaningful response | |
| if (data && (data.Abstract || data.Answer || data.Definition || data.RelatedTopics || data.Results)) { | |
| return data; | |
| } | |
| // If response exists but is empty, try next URL | |
| console.log('Empty response, trying next URL'); | |
| lastError = new Error('Empty response'); | |
| } catch (error) { | |
| console.log('Error with URL:', attemptUrl, error.message); | |
| lastError = error; | |
| continue; | |
| } | |
| } | |
| throw lastError || new Error('Failed to fetch results'); | |
| } | |
| /** | |
| * Handle user submission | |
| */ | |
| async function handleSubmit(event) { | |
| event.preventDefault(); | |
| const query = elements.userInput.value.trim(); | |
| if (!query || isLoading) return; | |
| // Add user message | |
| addMessage(query, true); | |
| elements.userInput.value = ''; | |
| // Set loading state | |
| isLoading = true; | |
| elements.sendButton.disabled = true; | |
| elements.userInput.disabled = true; | |
| const loadingMessage = createLoadingMessage(); | |
| elements.chatMessages.appendChild(loadingMessage); | |
| elements.chatMessages.parentElement.scrollTop = elements.chatMessages.parentElement.scrollHeight; | |
| updateStatus('Searching the web...', 'searching'); | |
| try { | |
| // Fetch from DuckDuckGo | |
| const data = await fetchDuckDuckGoAnswer(query); | |
| // Remove loading message | |
| loadingMessage.remove(); | |
| // Format and display response | |
| const formattedResponse = formatDuckDuckGoResponse(data); | |
| addMessage(formattedResponse, false); | |
| updateStatus('Ready to search', 'ready'); | |
| } catch (error) { | |
| // Remove loading message | |
| loadingMessage.remove(); | |
| // Show error message with retry suggestion | |
| const errorContent = document.createElement('div'); | |
| errorContent.innerHTML = ` | |
| <p>π Sorry, I couldn't find results for that query.</p> | |
| <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--text-secondary);"> | |
| ${error.message.includes('abort') ? 'Request timed out. Please try again.' : 'Try using different keywords or check your internet connection.'} | |
| </p> | |
| `; | |
| addMessage(errorContent, false); | |
| updateStatus('Error occurred', 'error'); | |
| console.error('Search Error:', error); | |
| } finally { | |
| // Reset loading state | |
| isLoading = false; | |
| elements.sendButton.disabled = false; | |
| elements.userInput.disabled = false; | |
| elements.userInput.focus(); | |
| } | |
| } | |
| /** | |
| * Handle example query clicks | |
| */ | |
| function setupExampleQueries() { | |
| elements.chatMessages.addEventListener('click', (event) => { | |
| if (event.target.tagName === 'LI') { | |
| const query = event.target.textContent; | |
| elements.userInput.value = query; | |
| elements.chatForm.dispatchEvent(new Event('submit')); | |
| } | |
| }); | |
| } | |
| /** | |
| * Initialize the application | |
| */ | |
| function init() { | |
| // Set up event listeners | |
| elements.chatForm.addEventListener('submit', handleSubmit); | |
| // Handle Enter key | |
| elements.userInput.addEventListener('keydown', (event) => { | |
| if (event.key === 'Enter' && !event.shiftKey) { | |
| event.preventDefault(); | |
| elements.chatForm.dispatchEvent(new Event('submit')); | |
| } | |
| }); | |
| // Set up example query clicks | |
| setupExampleQueries(); | |
| // Focus input | |
| elements.userInput.focus(); | |
| updateStatus('Ready to search', 'ready'); | |
| console.log('π AI Web Search Assistant initialized!'); | |
| console.log('Powered by DuckDuckGo Instant Answer API'); | |
| } | |
| // Start the app | |
| document.addEventListener('DOMContentLoaded', init); |