Files
archy/packages/app/vite-web-search.ts
T
Dorian 2d056f9498 feat(chat): enhance chat functionality with web search and article integration
- Updated ChatMessage and ChatWindow components to support inline web search results and articles.
- Integrated new web search and RSS plugins into the chat system for real-time information retrieval.
- Enhanced useContentPanel to manage web search results alongside existing media types.
- Added ArticleOverlay component for displaying selected articles from search results.
- Improved UI elements and styles for better user interaction with web search features.

Made-with: Cursor
2026-03-02 21:29:50 +00:00

136 lines
4.9 KiB
TypeScript

import type { Plugin } from 'vite'
import type { Connect } from 'vite'
import { loadEnv } from 'vite'
import { search as searchDuckDuckGo } from 'duck-duck-scrape'
export interface WebSearchResult {
title: string
url: string
content?: string
imgSrc?: string
engine?: string
}
const FALLBACK_INSTANCES = [
'https://searx.tiekoetter.com',
'https://search.bus-hit.me',
'https://paulgo.io',
]
async function fetchFromSearXNG(
searchUrl: string,
q: string,
): Promise<{ results?: { title?: string; url?: string; content?: string; img_src?: string; thumbnail?: string; engine?: string }[] } | null> {
try {
const searchRes = await fetch(searchUrl, {
headers: { Accept: 'application/json', 'User-Agent': 'AIUI/1.0' },
signal: AbortSignal.timeout(6000),
})
if (!searchRes.ok) return null
return (await searchRes.json()) as { results?: { title?: string; url?: string; content?: string; img_src?: string; thumbnail?: string; engine?: string }[] }
} catch {
return null
}
}
function createWebSearchMiddleware(searxUrl: string | undefined) {
return async (req: Connect.IncomingMessage, res: any, next: () => void) => {
if (req.method !== 'GET') return next()
const url = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`)
const q = url.searchParams.get('q')?.trim()
if (!q) {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Missing q (query)' }))
return
}
const instances = searxUrl ? [searxUrl] : FALLBACK_INSTANCES
let data: { results?: { title?: string; url?: string; content?: string; img_src?: string; thumbnail?: string; engine?: string }[] } | null = null
let lastError = ''
for (const baseUrl of instances) {
const normalized = baseUrl.replace(/\/$/, '')
const searchUrl = `${normalized}/search?${new URLSearchParams({ q, format: 'json', pageno: '1' })}`
console.log('[web-search]', q.slice(0, 40), '→', normalized)
data = await fetchFromSearXNG(searchUrl, q)
if (data?.results && data.results.length > 0) {
console.log('[web-search]', data.results.length, 'results from', normalized)
break
}
lastError = data ? 'no results' : 'request failed'
console.warn('[web-search]', normalized, lastError, '— trying next')
}
if (!data) {
console.warn('[web-search] SearXNG failed, trying DuckDuckGo fallback')
try {
const ddg = await searchDuckDuckGo(q)
if (ddg?.results?.length) {
const results: WebSearchResult[] = ddg.results
.filter((r: { title?: string; url?: string }) => r.title && r.url)
.slice(0, 6)
.map((r: { title: string; url: string; description?: string; icon?: string }) => ({
title: r.title,
url: r.url,
content: r.description ?? undefined,
imgSrc: r.icon ?? undefined,
}))
console.log('[web-search] DuckDuckGo fallback:', results.length, 'results')
res.setHeader('Content-Type', 'application/json')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Cache-Control', 'public, max-age=300')
res.end(JSON.stringify({ results }))
return
}
} catch (ddgErr) {
console.warn('[web-search] DuckDuckGo fallback failed:', ddgErr)
}
console.error('[web-search] All search backends failed:', lastError)
res.writeHead(502, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Web search unavailable. Try SEARXNG_URL in .env.local.' }))
return
}
try {
const results: WebSearchResult[] = (data.results ?? [])
.filter((r) => r.title && r.url)
.slice(0, 6)
.map((r) => ({
title: r.title ?? '',
url: r.url ?? '',
content: r.content ?? undefined,
imgSrc: r.img_src || r.thumbnail || undefined,
engine: r.engine ?? undefined,
}))
res.setHeader('Content-Type', 'application/json')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Cache-Control', 'public, max-age=300')
res.end(JSON.stringify({ results }))
} catch (err) {
console.error('[web-search]', err)
res.writeHead(502, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: String(err) }))
}
}
}
export function webSearchPlugin(): Plugin {
let searxUrl: string | undefined
return {
name: 'aiui-web-search',
configResolved(config) {
const env = loadEnv(config.mode, process.cwd(), '')
searxUrl = env.SEARXNG_URL ?? env.VITE_SEARXNG_URL
},
configureServer(server) {
server.middlewares.use('/api/web-search', createWebSearchMiddleware(searxUrl))
},
configurePreviewServer(server) {
server.middlewares.use('/api/web-search', createWebSearchMiddleware(searxUrl))
},
}
}