Spaces:
Paused
Paused
| import { PreviewServer, ViteDevServer, defineConfig } from "vite"; | |
| import react from "@vitejs/plugin-react"; | |
| import basicSSL from "@vitejs/plugin-basic-ssl"; | |
| import fetch from "node-fetch"; | |
| import { RateLimiterMemory } from "rate-limiter-flexible"; | |
| import { writeFileSync, readFileSync, existsSync } from "node:fs"; | |
| import temporaryDirectory from "temp-dir"; | |
| import path from "node:path"; | |
| import { | |
| initModel, | |
| distance as calculateSimilarity, | |
| EmbeddingsModel, | |
| } from "@energetic-ai/embeddings"; | |
| import { modelSource as embeddingModel } from "@energetic-ai/model-embeddings-en"; | |
| const serverStartTime = new Date().getTime(); | |
| let searchesSinceLastRestart = 0; | |
| export default defineConfig(({ command }) => { | |
| if (command === "build") regenerateSearchToken(); | |
| return { | |
| root: "./client", | |
| define: { | |
| VITE_SEARCH_TOKEN: JSON.stringify(getSearchToken()), | |
| }, | |
| server: { | |
| host: process.env.HOST, | |
| port: process.env.PORT ? Number(process.env.PORT) : undefined, | |
| hmr: { | |
| port: process.env.HMR_PORT ? Number(process.env.HMR_PORT) : undefined, | |
| }, | |
| }, | |
| preview: { | |
| host: process.env.HOST, | |
| port: process.env.PORT ? Number(process.env.PORT) : undefined, | |
| }, | |
| build: { | |
| target: "esnext", | |
| }, | |
| plugins: [ | |
| process.env.BASIC_SSL === "true" ? basicSSL() : undefined, | |
| react(), | |
| { | |
| name: "configure-server-cross-origin-isolation", | |
| configureServer: crossOriginServerHook, | |
| configurePreviewServer: crossOriginServerHook, | |
| }, | |
| { | |
| name: "configure-server-search-endpoint", | |
| configureServer: searchEndpointServerHook, | |
| configurePreviewServer: searchEndpointServerHook, | |
| }, | |
| { | |
| name: "configure-server-status-endpoint", | |
| configureServer: statusEndpointServerHook, | |
| configurePreviewServer: statusEndpointServerHook, | |
| }, | |
| { | |
| name: "configure-server-cache", | |
| configurePreviewServer: cacheServerHook, | |
| }, | |
| ], | |
| }; | |
| }); | |
| function crossOriginServerHook<T extends ViteDevServer | PreviewServer>( | |
| server: T, | |
| ) { | |
| server.middlewares.use((_, response, next) => { | |
| /** Server headers for cross origin isolation, which enable clients to use `SharedArrayBuffer` on the Browser. */ | |
| const crossOriginIsolationHeaders: { key: string; value: string }[] = [ | |
| { | |
| key: "Cross-Origin-Embedder-Policy", | |
| value: "require-corp", | |
| }, | |
| { | |
| key: "Cross-Origin-Opener-Policy", | |
| value: "same-origin", | |
| }, | |
| { | |
| key: "Cross-Origin-Resource-Policy", | |
| value: "cross-origin", | |
| }, | |
| ]; | |
| crossOriginIsolationHeaders.forEach(({ key, value }) => { | |
| response.setHeader(key, value); | |
| }); | |
| next(); | |
| }); | |
| } | |
| function statusEndpointServerHook<T extends ViteDevServer | PreviewServer>( | |
| server: T, | |
| ) { | |
| server.middlewares.use(async (request, response, next) => { | |
| if (!request.url.startsWith("/status")) return next(); | |
| const secondsSinceLastRestart = Math.floor( | |
| (new Date().getTime() - serverStartTime) / 1000, | |
| ); | |
| response.end( | |
| JSON.stringify({ | |
| secondsSinceLastRestart, | |
| searchesSinceLastRestart, | |
| searchesPerSecond: searchesSinceLastRestart / secondsSinceLastRestart, | |
| }), | |
| ); | |
| }); | |
| } | |
| function searchEndpointServerHook<T extends ViteDevServer | PreviewServer>( | |
| server: T, | |
| ) { | |
| const rateLimiter = new RateLimiterMemory({ | |
| points: 2, | |
| duration: 10, | |
| }); | |
| server.middlewares.use(async (request, response, next) => { | |
| if (!request.url.startsWith("/search")) return next(); | |
| const { searchParams } = new URL( | |
| request.url, | |
| `http://${request.headers.host}`, | |
| ); | |
| const token = searchParams.get("token"); | |
| if (!token || token !== getSearchToken()) { | |
| response.statusCode = 401; | |
| response.end("Unauthorized."); | |
| return; | |
| } | |
| const query = searchParams.get("q"); | |
| if (!query) { | |
| response.statusCode = 400; | |
| response.end("Missing the query parameter."); | |
| return; | |
| } | |
| const limitParam = searchParams.get("limit"); | |
| const limit = | |
| limitParam && Number(limitParam) > 0 ? Number(limitParam) : undefined; | |
| try { | |
| const remoteAddress = ( | |
| (request.headers["x-forwarded-for"] as string) || | |
| request.socket.remoteAddress || | |
| "unknown" | |
| ) | |
| .split(",")[0] | |
| .trim(); | |
| await rateLimiter.consume(remoteAddress); | |
| } catch (error) { | |
| response.statusCode = 429; | |
| response.end("Too many requests."); | |
| return; | |
| } | |
| const searchResults = await fetchSearXNG(query, limit); | |
| searchesSinceLastRestart++; | |
| if (searchResults.length === 0) { | |
| response.end(JSON.stringify([])); | |
| return; | |
| } | |
| try { | |
| response.end( | |
| JSON.stringify(await rankSearchResults(query, searchResults)), | |
| ); | |
| } catch (error) { | |
| console.error("Error ranking search results:", error); | |
| response.end(JSON.stringify(searchResults)); | |
| } | |
| }); | |
| } | |
| function cacheServerHook<T extends ViteDevServer | PreviewServer>(server: T) { | |
| server.middlewares.use(async (request, response, next) => { | |
| if ( | |
| request.url === "/" || | |
| request.url.startsWith("/?") || | |
| request.url.endsWith(".html") | |
| ) { | |
| response.setHeader("Cache-Control", "no-cache"); | |
| } else { | |
| response.setHeader("Cache-Control", "public, max-age=86400"); | |
| } | |
| next(); | |
| }); | |
| } | |
| async function fetchSearXNG(query: string, limit?: number) { | |
| try { | |
| const url = new URL("http://127.0.0.1:8080/search"); | |
| url.search = new URLSearchParams({ | |
| q: query, | |
| language: "auto", | |
| safesearch: "0", | |
| format: "json", | |
| }).toString(); | |
| const response = await fetch(url); | |
| let { results } = (await response.json()) as { | |
| results: { url: string; title: string; content: string }[]; | |
| }; | |
| const searchResults: [title: string, content: string, url: string][] = []; | |
| if (results) { | |
| if (limit && limit > 0) { | |
| results = results.slice(0, limit); | |
| } | |
| const uniqueUrls = new Set<string>(); | |
| for (const result of results) { | |
| if (!result.content || uniqueUrls.has(result.url)) continue; | |
| const stripHtmlTags = (str: string) => str.replace(/<[^>]*>?/gm, ""); | |
| const content = stripHtmlTags(result.content).trim(); | |
| if (content === "") continue; | |
| const title = stripHtmlTags(result.title); | |
| const url = result.url; | |
| searchResults.push([title, content, url]); | |
| uniqueUrls.add(url); | |
| } | |
| } | |
| return searchResults; | |
| } catch (e) { | |
| console.error(e); | |
| return []; | |
| } | |
| } | |
| function getSearchTokenFilePath() { | |
| return path.resolve(temporaryDirectory, "minisearch-token"); | |
| } | |
| function regenerateSearchToken() { | |
| const newToken = Math.random().toString(36).substring(2); | |
| writeFileSync(getSearchTokenFilePath(), newToken); | |
| } | |
| function getSearchToken() { | |
| if (!existsSync(getSearchTokenFilePath())) regenerateSearchToken(); | |
| return readFileSync(getSearchTokenFilePath(), "utf8"); | |
| } | |
| let embeddingModelInstance: EmbeddingsModel | undefined; | |
| async function getSimilarityScores(query: string, documents: string[]) { | |
| if (!embeddingModelInstance) { | |
| embeddingModelInstance = await initModel(embeddingModel); | |
| } | |
| const [queryEmbedding] = await embeddingModelInstance.embed([query]); | |
| const documentsEmbeddings = await embeddingModelInstance.embed(documents); | |
| return documentsEmbeddings.map((documentEmbedding) => | |
| calculateSimilarity(queryEmbedding, documentEmbedding), | |
| ); | |
| } | |
| async function rankSearchResults( | |
| query: string, | |
| searchResults: [title: string, content: string, url: string][], | |
| ) { | |
| const scores = await getSimilarityScores( | |
| query.toLocaleLowerCase(), | |
| searchResults.map(([title, snippet, url]) => | |
| `${title}\n${url}\n${snippet}`.toLocaleLowerCase(), | |
| ), | |
| ); | |
| const searchResultToScoreMap: Map<(typeof searchResults)[0], number> = | |
| new Map(); | |
| scores.map((score, index) => | |
| searchResultToScoreMap.set(searchResults[index], score ?? 0), | |
| ); | |
| return searchResults | |
| .slice() | |
| .sort( | |
| (a, b) => | |
| (searchResultToScoreMap.get(b) ?? 0) - | |
| (searchResultToScoreMap.get(a) ?? 0), | |
| ); | |
| } | |