feat: initial IQAI multi-model AI dashboard
- Express backend with Replicate API proxy (chat, models, account, search) - React + Vite + Tailwind frontend with custom Midnight Violet color scheme - @mention autocomplete to route messages to specific models - Parallel multi-model queries with model selection in sidebar - DuckDuckGo web search context injection - Model manager UI (add/edit/remove Replicate models) - Per-model system instructions per conversation - Replicate account info display in sidebar - Conversation history with local persistence (Zustand) - Full Docker deployment (backend + nginx-served frontend) - Montserrat + Poppins fonts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
117
backend/routes/search.js
Normal file
117
backend/routes/search.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// DuckDuckGo Instant Answer API - no key required
|
||||
async function ddgInstantAnswer(query) {
|
||||
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
||||
const res = await fetch(url, {
|
||||
headers: { 'User-Agent': 'IQAI-Dashboard/1.0' }
|
||||
});
|
||||
if (!res.ok) throw new Error('DDG search failed');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// DuckDuckGo HTML search scraper for more results
|
||||
async function ddgWebSearch(query) {
|
||||
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html'
|
||||
}
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const html = await res.text();
|
||||
|
||||
// Parse result links and snippets from DDG HTML
|
||||
const results = [];
|
||||
const resultRegex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
const snippetRegex = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
|
||||
const links = [...html.matchAll(resultRegex)].slice(0, 8);
|
||||
const snippets = [...html.matchAll(snippetRegex)].slice(0, 8);
|
||||
|
||||
for (let i = 0; i < Math.min(links.length, 5); i++) {
|
||||
const title = links[i][2].replace(/<[^>]+>/g, '').trim();
|
||||
const snippet = snippets[i] ? snippets[i][1].replace(/<[^>]+>/g, '').trim() : '';
|
||||
const href = links[i][1];
|
||||
// DDG redirects through their own URLs, extract the real URL
|
||||
const realUrl = href.startsWith('//duckduckgo.com/l/?uddg=')
|
||||
? decodeURIComponent(href.split('uddg=')[1]?.split('&')[0] || href)
|
||||
: href;
|
||||
if (title && !title.includes('DuckDuckGo')) {
|
||||
results.push({ title, snippet, url: realUrl });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const { q } = req.query;
|
||||
if (!q) return res.status(400).json({ error: 'Query parameter q is required' });
|
||||
|
||||
try {
|
||||
const [instant, webResults] = await Promise.allSettled([
|
||||
ddgInstantAnswer(q),
|
||||
ddgWebSearch(q)
|
||||
]);
|
||||
|
||||
const answer = instant.status === 'fulfilled' ? instant.value : null;
|
||||
const results = webResults.status === 'fulfilled' ? webResults.value : [];
|
||||
|
||||
// Format for injection into AI prompt
|
||||
const formatted = formatSearchResults(q, answer, results);
|
||||
|
||||
res.json({
|
||||
query: q,
|
||||
answer,
|
||||
results,
|
||||
formatted
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
function formatSearchResults(query, instant, webResults) {
|
||||
const lines = [`[WEB SEARCH RESULTS FOR: "${query}"]`, ''];
|
||||
|
||||
if (instant?.AbstractText) {
|
||||
lines.push(`Summary: ${instant.AbstractText}`);
|
||||
if (instant.AbstractSource) lines.push(`Source: ${instant.AbstractSource}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (instant?.Answer) {
|
||||
lines.push(`Direct Answer: ${instant.Answer}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (webResults.length > 0) {
|
||||
lines.push('Web Results:');
|
||||
webResults.forEach((r, i) => {
|
||||
lines.push(`${i + 1}. ${r.title}`);
|
||||
if (r.snippet) lines.push(` ${r.snippet}`);
|
||||
lines.push(` URL: ${r.url}`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (instant?.RelatedTopics?.length > 0) {
|
||||
const topics = instant.RelatedTopics
|
||||
.filter(t => t.Text)
|
||||
.slice(0, 3)
|
||||
.map(t => `- ${t.Text}`);
|
||||
if (topics.length > 0) {
|
||||
lines.push('Related Topics:');
|
||||
lines.push(...topics);
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('[END SEARCH RESULTS]');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export { router as searchRouter };
|
||||
Reference in New Issue
Block a user