Initial commit

This commit is contained in:
Ammaar Reshi
2025-01-04 14:06:53 +00:00
parent 7082408604
commit d6025af146
23760 changed files with 3299690 additions and 0 deletions

27
server/env.ts Normal file
View File

@ -0,0 +1,27 @@
import dotenv from "dotenv";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const envPath = path.resolve(__dirname, "../.env");
export function setupEnvironment() {
const result = dotenv.config({ path: envPath });
if (result.error) {
throw new Error(
`Failed to load .env file from ${envPath}: ${result.error.message}`
);
}
if (!process.env.GOOGLE_API_KEY) {
throw new Error(
"GOOGLE_API_KEY environment variable must be set in .env file"
);
}
return {
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
NODE_ENV: process.env.NODE_ENV || "development",
};
}

78
server/index.ts Normal file
View File

@ -0,0 +1,78 @@
import { setupEnvironment } from "./env";
import path from "path";
import { fileURLToPath } from "url";
import express, { type Request, Response, NextFunction } from "express";
import { registerRoutes } from "./routes";
import { setupVite, serveStatic, log } from "./vite";
// Setup environment variables first
const env = setupEnvironment();
console.log("\n--- Environment Setup Debug ---");
console.log("Environment variables loaded:", env);
console.log("--- End Debug ---\n");
// Get the directory name properly with ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: Record<string, any> | undefined = undefined;
const originalResJson = res.json;
res.json = function (bodyJson, ...args) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
(async () => {
const server = registerRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// importantly only setup vite in development and after
// setting up all the other routes so the catch-all route
// doesn't interfere with the other routes
if (app.get("env") === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
// ALWAYS serve the app on port 3000
// this serves both the API and the client
const PORT = 3000;
server.listen(PORT, "0.0.0.0", () => {
log(`serving on port ${PORT}`);
});
})();

290
server/routes.ts Normal file
View File

@ -0,0 +1,290 @@
import type { Express } from "express";
import { createServer, type Server } from "http";
import {
GoogleGenerativeAI,
type ChatSession,
type GenerateContentResult,
} from "@google/generative-ai";
import { marked } from "marked";
import { setupEnvironment } from "./env";
const env = setupEnvironment();
const genAI = new GoogleGenerativeAI(env.GOOGLE_API_KEY);
const model = genAI.getGenerativeModel({
model: "gemini-2.0-flash-exp",
generationConfig: {
temperature: 0.9,
topP: 1,
topK: 1,
maxOutputTokens: 2048,
},
});
// Store chat sessions in memory
const chatSessions = new Map<string, ChatSession>();
// Format raw text into proper markdown
async function formatResponseToMarkdown(
text: string | Promise<string>
): Promise<string> {
// Ensure we have a string to work with
const resolvedText = await Promise.resolve(text);
// First, ensure consistent newlines
let processedText = resolvedText.replace(/\r\n/g, "\n");
// Process main sections (lines that start with word(s) followed by colon)
processedText = processedText.replace(
/^([A-Za-z][A-Za-z\s]+):(\s*)/gm,
"## $1$2"
);
// Process sub-sections (any remaining word(s) followed by colon within text)
processedText = processedText.replace(
/(?<=\n|^)([A-Za-z][A-Za-z\s]+):(?!\d)/gm,
"### $1"
);
// Process bullet points
processedText = processedText.replace(/^[•●○]\s*/gm, "* ");
// Split into paragraphs
const paragraphs = processedText.split("\n\n").filter(Boolean);
// Process each paragraph
const formatted = paragraphs
.map((p) => {
// If it's a header or list item, preserve it
if (p.startsWith("#") || p.startsWith("*") || p.startsWith("-")) {
return p;
}
// Add proper paragraph formatting
return `${p}\n`;
})
.join("\n\n");
// Configure marked options for better header rendering
marked.setOptions({
gfm: true,
breaks: true,
});
// Convert markdown to HTML using marked
return marked.parse(formatted);
}
interface WebSource {
uri: string;
title: string;
}
interface GroundingChunk {
web?: WebSource;
}
interface TextSegment {
startIndex: number;
endIndex: number;
text: string;
}
interface GroundingSupport {
segment: TextSegment;
groundingChunkIndices: number[];
confidenceScores: number[];
}
interface GroundingMetadata {
groundingChunks: GroundingChunk[];
groundingSupports: GroundingSupport[];
searchEntryPoint?: any;
webSearchQueries?: string[];
}
export function registerRoutes(app: Express): Server {
// Search endpoint - creates a new chat session
app.get("/api/search", async (req, res) => {
try {
const query = req.query.q as string;
if (!query) {
return res.status(400).json({
message: "Query parameter 'q' is required",
});
}
// Create a new chat session with search capability
const chat = model.startChat({
tools: [
{
// @ts-ignore - google_search is a valid tool but not typed in the SDK yet
google_search: {},
},
],
});
// Generate content with search tool
const result = await chat.sendMessage(query);
const response = await result.response;
console.log(
"Raw Google API Response:",
JSON.stringify(
{
text: response.text(),
candidates: response.candidates,
groundingMetadata: response.candidates?.[0]?.groundingMetadata,
},
null,
2
)
);
const text = response.text();
// Format the response text to proper markdown/HTML
const formattedText = await formatResponseToMarkdown(text);
// Extract sources from grounding metadata
const sourceMap = new Map<
string,
{ title: string; url: string; snippet: string }
>();
// Get grounding metadata from response
const metadata = response.candidates?.[0]?.groundingMetadata as any;
if (metadata) {
const chunks = metadata.groundingChunks || [];
const supports = metadata.groundingSupports || [];
chunks.forEach((chunk: any, index: number) => {
if (chunk.web?.uri && chunk.web?.title) {
const url = chunk.web.uri;
if (!sourceMap.has(url)) {
// Find snippets that reference this chunk
const snippets = supports
.filter((support: any) =>
support.groundingChunkIndices.includes(index)
)
.map((support: any) => support.segment.text)
.join(" ");
sourceMap.set(url, {
title: chunk.web.title,
url: url,
snippet: snippets || "",
});
}
}
});
}
const sources = Array.from(sourceMap.values());
// Generate a session ID and store the chat
const sessionId = Math.random().toString(36).substring(7);
chatSessions.set(sessionId, chat);
res.json({
sessionId,
summary: formattedText,
sources,
});
} catch (error: any) {
console.error("Search error:", error);
res.status(500).json({
message:
error.message || "An error occurred while processing your search",
});
}
});
// Follow-up endpoint - continues existing chat session
app.post("/api/follow-up", async (req, res) => {
try {
const { sessionId, query } = req.body;
if (!sessionId || !query) {
return res.status(400).json({
message: "Both sessionId and query are required",
});
}
const chat = chatSessions.get(sessionId);
if (!chat) {
return res.status(404).json({
message: "Chat session not found",
});
}
// Send follow-up message in existing chat
const result = await chat.sendMessage(query);
const response = await result.response;
console.log(
"Raw Google API Follow-up Response:",
JSON.stringify(
{
text: response.text(),
candidates: response.candidates,
groundingMetadata: response.candidates?.[0]?.groundingMetadata,
},
null,
2
)
);
const text = response.text();
// Format the response text to proper markdown/HTML
const formattedText = await formatResponseToMarkdown(text);
// Extract sources from grounding metadata
const sourceMap = new Map<
string,
{ title: string; url: string; snippet: string }
>();
// Get grounding metadata from response
const metadata = response.candidates?.[0]?.groundingMetadata as any;
if (metadata) {
const chunks = metadata.groundingChunks || [];
const supports = metadata.groundingSupports || [];
chunks.forEach((chunk: any, index: number) => {
if (chunk.web?.uri && chunk.web?.title) {
const url = chunk.web.uri;
if (!sourceMap.has(url)) {
// Find snippets that reference this chunk
const snippets = supports
.filter((support: any) =>
support.groundingChunkIndices.includes(index)
)
.map((support: any) => support.segment.text)
.join(" ");
sourceMap.set(url, {
title: chunk.web.title,
url: url,
snippet: snippets || "",
});
}
}
});
}
const sources = Array.from(sourceMap.values());
res.json({
summary: formattedText,
sources,
});
} catch (error: any) {
console.error("Follow-up error:", error);
res.status(500).json({
message:
error.message ||
"An error occurred while processing your follow-up question",
});
}
});
const httpServer = createServer(app);
return httpServer;
}

93
server/vite.ts Normal file
View File

@ -0,0 +1,93 @@
import express, { type Express } from "express";
import fs from "fs";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import { createServer as createViteServer, createLogger } from "vite";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import { type Server } from "http";
import viteConfig from "../vite.config";
const viteLogger = createLogger();
export function log(message: string, source = "express") {
const formattedTime = new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
console.log(`${formattedTime} [${source}] ${message}`);
}
export async function setupVite(app: Express, server: Server) {
const vite = await createViteServer({
...viteConfig,
configFile: false,
customLogger: {
...viteLogger,
error: (msg, options) => {
if (
msg.includes("[TypeScript] Found 0 errors. Watching for file changes")
) {
log("no errors found", "tsc");
return;
}
if (msg.includes("[TypeScript] ")) {
const [errors, summary] = msg.split("[TypeScript] ", 2);
log(`${summary} ${errors}\u001b[0m`, "tsc");
return;
} else {
viteLogger.error(msg, options);
process.exit(1);
}
},
},
server: {
middlewareMode: true,
hmr: { server },
},
appType: "custom",
});
app.use(vite.middlewares);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
try {
const clientTemplate = path.resolve(
__dirname,
"..",
"client",
"index.html",
);
// always reload the index.html file from disk incase it changes
const template = await fs.promises.readFile(clientTemplate, "utf-8");
const page = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(page);
} catch (e) {
vite.ssrFixStacktrace(e as Error);
next(e);
}
});
}
export function serveStatic(app: Express) {
const distPath = path.resolve(__dirname, "public");
if (!fs.existsSync(distPath)) {
throw new Error(
`Could not find the build directory: ${distPath}, make sure to build the client first`,
);
}
app.use(express.static(distPath));
// fall through to index.html if the file doesn't exist
app.use("*", (_req, res) => {
res.sendFile(path.resolve(distPath, "index.html"));
});
}