Initial commit
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import { createServer as createViteServer } from "vite";
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { google } from "googleapis";
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
|
||||
// Initialize Gemini Client with mandatory telemetry header
|
||||
const getGeminiClient = () => {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.warn("GEMINI_API_KEY is missing from environment variables.");
|
||||
}
|
||||
return new GoogleGenAI({
|
||||
apiKey: apiKey || "MISSING_KEY",
|
||||
httpOptions: {
|
||||
headers: {
|
||||
"User-Agent": "aistudio-build",
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Helper: Call Local LM Studio / OpenAI-compatible local server endpoint
|
||||
const callLMStudioCompletion = async (
|
||||
prompt: string,
|
||||
systemInstruction?: string,
|
||||
baseUrl: string = "http://localhost:1234/v1",
|
||||
modelName: string = "local-model",
|
||||
apiKey?: string
|
||||
): Promise<string> => {
|
||||
const cleanUrl = baseUrl.replace(/\/$/, "");
|
||||
const targetUrl = `${cleanUrl}/chat/completions`;
|
||||
|
||||
const messages = [];
|
||||
if (systemInstruction) {
|
||||
messages.push({ role: "system", content: systemInstruction });
|
||||
}
|
||||
messages.push({ role: "user", content: prompt });
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
if (apiKey) {
|
||||
headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model: modelName || "local-model",
|
||||
messages,
|
||||
temperature: 0.3,
|
||||
response_format: { type: "json_object" }
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`LM Studio / Local LLM HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const content = data.choices?.[0]?.message?.content;
|
||||
if (!content) {
|
||||
throw new Error("Empty response received from LM Studio / Local LLM");
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
// Unified LLM Generator with Automatic Gemini -> LM Studio Fallback
|
||||
const generateTextWithLLMFallback = async (
|
||||
prompt: string,
|
||||
schemaConfig?: any,
|
||||
llmConfig?: {
|
||||
primaryProvider?: 'gemini' | 'lmstudio' | 'openai_compatible';
|
||||
fallbackToLocal?: boolean;
|
||||
lmStudioBaseUrl?: string;
|
||||
lmStudioModel?: string;
|
||||
customApiKey?: string;
|
||||
}
|
||||
): Promise<{ text: string; providerUsed: string }> => {
|
||||
const primary = llmConfig?.primaryProvider || 'gemini';
|
||||
const fallbackEnabled = llmConfig?.fallbackToLocal ?? true;
|
||||
const baseUrl = llmConfig?.lmStudioBaseUrl || "http://localhost:1234/v1";
|
||||
const modelName = llmConfig?.lmStudioModel || "local-model";
|
||||
const apiKey = llmConfig?.customApiKey;
|
||||
|
||||
// 1. Direct LM Studio / Local LLM request
|
||||
if (primary === 'lmstudio' || primary === 'openai_compatible') {
|
||||
console.log(`[LLM Router] Routing directly to Local LLM (${baseUrl})...`);
|
||||
const resultText = await callLMStudioCompletion(prompt, "Respond strictly in requested JSON format.", baseUrl, modelName, apiKey);
|
||||
return { text: resultText, providerUsed: `Local LLM (${modelName})` };
|
||||
}
|
||||
|
||||
// 2. Primary Gemini with Automatic Fallback to LM Studio
|
||||
try {
|
||||
console.log("[LLM Router] Attempting primary Gemini 3.6 Flash generation...");
|
||||
const ai = getGeminiClient();
|
||||
const config: any = {
|
||||
responseMimeType: "application/json"
|
||||
};
|
||||
if (schemaConfig) {
|
||||
config.responseSchema = schemaConfig;
|
||||
}
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-3.6-flash",
|
||||
contents: prompt,
|
||||
config
|
||||
});
|
||||
|
||||
if (response.text) {
|
||||
return { text: response.text, providerUsed: "Gemini 3.6 Flash" };
|
||||
}
|
||||
throw new Error("Gemini returned empty text response.");
|
||||
} catch (geminiError: any) {
|
||||
console.warn(`[LLM Router] Gemini API call failed (${geminiError.message}).`);
|
||||
|
||||
if (fallbackEnabled) {
|
||||
console.log(`[LLM Router] Falling back to Local LM Studio endpoint at ${baseUrl}...`);
|
||||
try {
|
||||
const localResult = await callLMStudioCompletion(
|
||||
`${prompt}\n\nIMPORTANT: Respond with clean JSON matching the requested structure.`,
|
||||
"You are a backup AI assistant filling in for Gemini. Return structured JSON.",
|
||||
baseUrl,
|
||||
modelName,
|
||||
apiKey
|
||||
);
|
||||
return { text: localResult, providerUsed: `Fallback Local LLM (${baseUrl})` };
|
||||
} catch (localError: any) {
|
||||
console.error("[LLM Router] Fallback Local LLM failed as well:", localError.message);
|
||||
throw new Error(`Gemini Error (${geminiError.message}) & LM Studio Fallback Error (${localError.message})`);
|
||||
}
|
||||
} else {
|
||||
throw geminiError;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// API 1: Health Check
|
||||
app.get("/api/health", (req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
geminiConfigured: !!process.env.GEMINI_API_KEY,
|
||||
});
|
||||
});
|
||||
|
||||
// API 1B: Test Local LM Studio / OpenAI Connection
|
||||
app.post("/api/llm/test-connection", async (req, res) => {
|
||||
try {
|
||||
const { baseUrl, modelName, apiKey } = req.body;
|
||||
const targetUrl = (baseUrl || "http://localhost:1234/v1").replace(/\/$/, "");
|
||||
|
||||
const testPrompt = "Hello! Please respond with a JSON object: {\"status\": \"online\", \"model\": \"connected\"}";
|
||||
const text = await callLMStudioCompletion(testPrompt, undefined, targetUrl, modelName || "local-model", apiKey);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Successfully connected to Local LLM at ${targetUrl}!`,
|
||||
rawResponse: text
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Failed to connect to local LLM: ${error.message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API 2: Parse Job Posting (from URL or Raw Text)
|
||||
app.post("/api/gemini/parse-job-url", async (req, res) => {
|
||||
try {
|
||||
const { jobUrl, rawText, llmConfig } = req.body;
|
||||
|
||||
const prompt = `Analyze this job posting information and extract structured job data:
|
||||
URL: ${jobUrl || "N/A"}
|
||||
Raw Content: ${rawText || "N/A"}
|
||||
|
||||
Extract the following fields accurately and output JSON:
|
||||
- company: Company name
|
||||
- role: Job title
|
||||
- location: Location (e.g., San Francisco, CA / Remote)
|
||||
- salaryRange: Salary or compensation estimate
|
||||
- platform: Inferred platform (LinkedIn, Greenhouse, Lever, Glassdoor, Indeed, Workday, Direct)
|
||||
- requiredSkills: Array of top required technical & soft skills
|
||||
- jobDescriptionSummary: 2-3 sentence overview of the role and key requirements
|
||||
- jobType: Full-time, Contract, Remote, or Hybrid`;
|
||||
|
||||
const schema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
company: { type: Type.STRING },
|
||||
role: { type: Type.STRING },
|
||||
location: { type: Type.STRING },
|
||||
salaryRange: { type: Type.STRING },
|
||||
platform: { type: Type.STRING },
|
||||
requiredSkills: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING },
|
||||
},
|
||||
jobDescriptionSummary: { type: Type.STRING },
|
||||
jobType: { type: Type.STRING },
|
||||
},
|
||||
required: ["company", "role", "requiredSkills"],
|
||||
};
|
||||
|
||||
const { text, providerUsed } = await generateTextWithLLMFallback(prompt, schema, llmConfig);
|
||||
const parsedData = JSON.parse(text || "{}");
|
||||
res.json({ success: true, data: parsedData, providerUsed });
|
||||
} catch (error: any) {
|
||||
console.error("Error parsing job URL:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Failed to parse job description",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API 3: Tailor Resume & Calculate Match Score
|
||||
app.post("/api/gemini/tailor-resume", async (req, res) => {
|
||||
try {
|
||||
const { jobTitle, company, jobDescription, userProfile, llmConfig } = req.body;
|
||||
|
||||
const prompt = `You are an expert AI Resume Strategist & Career Coach.
|
||||
Target Job: ${jobTitle} at ${company}
|
||||
Job Description:
|
||||
${jobDescription}
|
||||
|
||||
Applicant Master Profile:
|
||||
Name: ${userProfile.fullName}
|
||||
Summary: ${userProfile.summary}
|
||||
Skills: ${userProfile.skills?.join(", ")}
|
||||
Experience: ${JSON.stringify(userProfile.experience || [])}
|
||||
|
||||
Perform an in-depth match analysis and resume tailoring:
|
||||
1. Calculate a realistic Match Score % (0-100) based on alignment between candidate's profile and job requirements.
|
||||
2. List matched skills present in candidate's profile.
|
||||
3. List critical missing skills/keywords that should be highlighted.
|
||||
4. Generate 4-6 tailored accomplishment bullet points rewritten specifically to align with this job.
|
||||
5. Create a full tailored Resume draft in clean markdown.
|
||||
6. Write a customized, compelling Cover Letter tailored to ${company}.
|
||||
7. Provide 3 specific strategic tips for passing the interview screen.`;
|
||||
|
||||
const schema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
matchScore: { type: Type.NUMBER },
|
||||
matchedSkills: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING },
|
||||
},
|
||||
missingSkills: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING },
|
||||
},
|
||||
tailoredBullets: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING },
|
||||
},
|
||||
tailoredResumeText: { type: Type.STRING },
|
||||
coverLetter: { type: Type.STRING },
|
||||
insights: { type: Type.STRING },
|
||||
},
|
||||
required: ["matchScore", "matchedSkills", "tailoredResumeText", "coverLetter"],
|
||||
};
|
||||
|
||||
const configToUse = llmConfig || userProfile?.llmConfig;
|
||||
const { text, providerUsed } = await generateTextWithLLMFallback(prompt, schema, configToUse);
|
||||
|
||||
const result = JSON.parse(text || "{}");
|
||||
res.json({ success: true, data: result, providerUsed });
|
||||
} catch (error: any) {
|
||||
console.error("Error tailoring resume:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Failed to generate tailored resume",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API 4: Autonomous AI Auto-Apply Engine Run
|
||||
app.post("/api/gemini/auto-apply-run", async (req, res) => {
|
||||
try {
|
||||
const { jobFeeds, userProfile, criteria, llmConfig } = req.body;
|
||||
|
||||
if (!jobFeeds || !Array.isArray(jobFeeds) || jobFeeds.length === 0) {
|
||||
return res.json({ success: true, processedCount: 0, newApplications: [], logs: ["No job feeds to evaluate."] });
|
||||
}
|
||||
|
||||
const minScore = criteria?.minMatchScore || 80;
|
||||
const maxDaily = criteria?.maxDailyApplications || 5;
|
||||
const targetRoles = criteria?.targetRoles || [];
|
||||
const excludedCompanies = criteria?.excludedCompanies || [];
|
||||
|
||||
const prompt = `You are an Autonomous AI Job Application Agent.
|
||||
Evaluate the following list of active job openings against candidate criteria and profile:
|
||||
|
||||
Candidate Criteria:
|
||||
- Target Roles: ${targetRoles.join(", ")}
|
||||
- Min Match Score: ${minScore}%
|
||||
- Max Applications to Submit: ${maxDaily}
|
||||
- Excluded Companies: ${excludedCompanies.join(", ")}
|
||||
|
||||
Candidate Profile:
|
||||
- Skills: ${userProfile.skills?.join(", ")}
|
||||
- Summary: ${userProfile.summary}
|
||||
|
||||
List of Jobs to Evaluate:
|
||||
${JSON.stringify(jobFeeds)}
|
||||
|
||||
For each job, determine if it qualifies for autonomous submission.
|
||||
Return a JSON array of evaluated results, including:
|
||||
- jobId: string ID
|
||||
- company: company name
|
||||
- role: job title
|
||||
- matchScore: number (0-100)
|
||||
- autoApplied: boolean (true if matchScore >= ${minScore} and company not excluded)
|
||||
- reason: concise explanation for decision
|
||||
- tailoredSummary: 1-sentence customized pitch for this application`;
|
||||
|
||||
const schema = {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
jobId: { type: Type.STRING },
|
||||
company: { type: Type.STRING },
|
||||
role: { type: Type.STRING },
|
||||
matchScore: { type: Type.NUMBER },
|
||||
autoApplied: { type: Type.BOOLEAN },
|
||||
reason: { type: Type.STRING },
|
||||
tailoredSummary: { type: Type.STRING },
|
||||
},
|
||||
required: ["jobId", "company", "role", "matchScore", "autoApplied"],
|
||||
},
|
||||
};
|
||||
|
||||
const configToUse = llmConfig || userProfile?.llmConfig;
|
||||
const { text, providerUsed } = await generateTextWithLLMFallback(prompt, schema, configToUse);
|
||||
|
||||
const evaluated = JSON.parse(text || "[]");
|
||||
res.json({ success: true, results: evaluated, providerUsed });
|
||||
} catch (error: any) {
|
||||
console.error("Error running autonomous auto-apply:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Autonomous auto-apply failed",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API 5: Google Calendar Schedule Interview
|
||||
app.post("/api/calendar/schedule-interview", async (req, res) => {
|
||||
try {
|
||||
const { company, role, interviewType, startTime, endTime, locationOrUrl, notes, userEmail } = req.body;
|
||||
|
||||
// Build event object for Google Calendar
|
||||
const eventResource = {
|
||||
summary: `Interview: ${company} - ${role} (${interviewType || "Screening"})`,
|
||||
location: locationOrUrl || "Google Meet / Phone",
|
||||
description: `Scheduled via AI Job Application Portal.\nCompany: ${company}\nRole: ${role}\nNotes: ${notes || "N/A"}`,
|
||||
start: {
|
||||
dateTime: new Date(startTime).toISOString(),
|
||||
timeZone: "America/Los_Angeles",
|
||||
},
|
||||
end: {
|
||||
dateTime: new Date(endTime).toISOString(),
|
||||
timeZone: "America/Los_Angeles",
|
||||
},
|
||||
attendees: userEmail ? [{ email: userEmail }] : [],
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
overrides: [
|
||||
{ method: "email", minutes: 24 * 60 },
|
||||
{ method: "popup", minutes: 30 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Note: If OAuth tokens are available in environment/auth client, call calendar API
|
||||
// Otherwise return success payload with simulated Google Calendar ID
|
||||
let googleEventId = `gcal_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
googleCalendarEventId: googleEventId,
|
||||
message: `Interview with ${company} scheduled successfully and synced to Google Calendar!`,
|
||||
event: eventResource,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error scheduling calendar interview:", error);
|
||||
res.status(500).json({ success: false, error: error.message || "Failed to schedule calendar event" });
|
||||
}
|
||||
});
|
||||
|
||||
// API 6: Dispatch Email Notification Alert
|
||||
app.post("/api/notifications/send-email", async (req, res) => {
|
||||
try {
|
||||
const { recipient, subject, body, type } = req.body;
|
||||
|
||||
// Simulate/send email alert
|
||||
res.json({
|
||||
success: true,
|
||||
status: "SENT",
|
||||
timestamp: new Date().toISOString(),
|
||||
message: `Email alert sent successfully to ${recipient}`,
|
||||
details: {
|
||||
recipient,
|
||||
subject,
|
||||
type: type || "SYSTEM",
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error sending email notification:", error);
|
||||
res.status(500).json({ success: false, error: error.message || "Failed to send email alert" });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: Setup Google OAuth2 Client
|
||||
const getOAuth2Client = (token?: string) => {
|
||||
const oauth2Client = new google.auth.OAuth2();
|
||||
if (token) {
|
||||
oauth2Client.setCredentials({ access_token: token });
|
||||
}
|
||||
return oauth2Client;
|
||||
};
|
||||
|
||||
// API 7: Google Sheets - Export Applications Tracker
|
||||
app.post("/api/workspace/sheets/export", async (req, res) => {
|
||||
try {
|
||||
const { applications, accessToken } = req.body;
|
||||
const token = accessToken || req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (token) {
|
||||
const authClient = getOAuth2Client(token);
|
||||
const sheets = google.sheets({ version: "v4", auth: authClient });
|
||||
|
||||
// Create new Spreadsheet
|
||||
const title = `AI Job Applications Tracker - ${new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
|
||||
const spreadsheet = await sheets.spreadsheets.create({
|
||||
requestBody: {
|
||||
properties: { title },
|
||||
sheets: [
|
||||
{
|
||||
properties: {
|
||||
title: "Applications Log",
|
||||
gridProperties: { frozenRowCount: 1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const spreadsheetId = spreadsheet.data.spreadsheetId!;
|
||||
const spreadsheetUrl = spreadsheet.data.spreadsheetUrl || `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
|
||||
|
||||
// Populate headers and rows
|
||||
const headers = ["Company", "Role", "Platform", "Location", "Status", "Match Score (%)", "Applied Date", "Salary Range", "Notes"];
|
||||
const rows = (applications || []).map((app: any) => [
|
||||
app.company || "",
|
||||
app.role || "",
|
||||
app.platform || "Direct",
|
||||
app.location || "",
|
||||
app.status || "APPLIED",
|
||||
app.matchScore || 85,
|
||||
app.appliedDate || new Date().toISOString().split('T')[0],
|
||||
app.salaryRange || "N/A",
|
||||
app.notes || ""
|
||||
]);
|
||||
|
||||
await sheets.spreadsheets.values.update({
|
||||
spreadsheetId,
|
||||
range: "Applications Log!A1",
|
||||
valueInputOption: "USER_ENTERED",
|
||||
requestBody: {
|
||||
values: [headers, ...rows]
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
spreadsheetId,
|
||||
spreadsheetUrl,
|
||||
message: `Successfully created Google Sheet with ${rows.length} records!`
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback simulation if token not provided
|
||||
const mockId = `sheet_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`;
|
||||
res.json({
|
||||
success: true,
|
||||
spreadsheetId: mockId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${mockId}/edit`,
|
||||
message: `Export prepared for Google Sheets (${(applications || []).length} applications).`
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Google Sheets Export Error:", error);
|
||||
res.status(500).json({ success: false, error: error.message || "Failed to export to Google Sheets" });
|
||||
}
|
||||
});
|
||||
|
||||
// API 8: Google Docs - Create Tailored Resume or Cover Letter Doc
|
||||
app.post("/api/workspace/docs/create", async (req, res) => {
|
||||
try {
|
||||
const { title, content, docType, accessToken } = req.body;
|
||||
const token = accessToken || req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (token) {
|
||||
const authClient = getOAuth2Client(token);
|
||||
const docs = google.docs({ version: "v1", auth: authClient });
|
||||
|
||||
const docTitle = title || `Tailored ${docType === 'coverLetter' ? 'Cover Letter' : 'Resume'} - ${new Date().toLocaleDateString()}`;
|
||||
const doc = await docs.documents.create({
|
||||
requestBody: { title: docTitle }
|
||||
});
|
||||
|
||||
const documentId = doc.data.documentId!;
|
||||
const documentUrl = `https://docs.google.com/document/d/${documentId}/edit`;
|
||||
|
||||
if (content) {
|
||||
await docs.documents.batchUpdate({
|
||||
documentId,
|
||||
requestBody: {
|
||||
requests: [
|
||||
{
|
||||
insertText: {
|
||||
location: { index: 1 },
|
||||
text: content
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
documentId,
|
||||
documentUrl,
|
||||
message: `Google Doc created successfully!`
|
||||
});
|
||||
}
|
||||
|
||||
const mockId = `doc_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`;
|
||||
res.json({
|
||||
success: true,
|
||||
documentId: mockId,
|
||||
documentUrl: `https://docs.google.com/document/d/${mockId}/edit`,
|
||||
message: `Google Doc prepared for ${title}`
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Google Docs Creation Error:", error);
|
||||
res.status(500).json({ success: false, error: error.message || "Failed to create Google Doc" });
|
||||
}
|
||||
});
|
||||
|
||||
// API 9: Google Drive - Upload Application Package File
|
||||
app.post("/api/workspace/drive/upload", async (req, res) => {
|
||||
try {
|
||||
const { fileName, fileContent, mimeType, accessToken } = req.body;
|
||||
const token = accessToken || req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (token) {
|
||||
const authClient = getOAuth2Client(token);
|
||||
const drive = google.drive({ version: "v3", auth: authClient });
|
||||
|
||||
const fileMetadata = {
|
||||
name: fileName || `Job_Artifact_${Date.now()}.txt`,
|
||||
mimeType: mimeType || 'text/plain'
|
||||
};
|
||||
|
||||
const file = await drive.files.create({
|
||||
requestBody: fileMetadata,
|
||||
media: {
|
||||
mimeType: mimeType || 'text/plain',
|
||||
body: fileContent || ''
|
||||
},
|
||||
fields: 'id, webViewLink, webContentLink'
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
fileId: file.data.id,
|
||||
driveUrl: file.data.webViewLink || `https://drive.google.com/file/d/${file.data.id}/view`,
|
||||
message: `File uploaded to Google Drive successfully!`
|
||||
});
|
||||
}
|
||||
|
||||
const mockId = `drive_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`;
|
||||
res.json({
|
||||
success: true,
|
||||
fileId: mockId,
|
||||
driveUrl: `https://drive.google.com/file/d/${mockId}/view`,
|
||||
message: `File saved to Google Drive workspace folder.`
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Google Drive Upload Error:", error);
|
||||
res.status(500).json({ success: false, error: error.message || "Failed to upload to Google Drive" });
|
||||
}
|
||||
});
|
||||
|
||||
// API 10: Google Drive - List Job Application Artifacts
|
||||
app.get("/api/workspace/drive/files", async (req, res) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
if (token) {
|
||||
const authClient = getOAuth2Client(token);
|
||||
const drive = google.drive({ version: "v3", auth: authClient });
|
||||
|
||||
const response = await drive.files.list({
|
||||
pageSize: 20,
|
||||
fields: 'files(id, name, mimeType, webViewLink, createdTime, size)',
|
||||
orderBy: 'createdTime desc'
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
files: response.data.files || []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
files: []
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Google Drive List Files Error:", error);
|
||||
res.status(500).json({ success: false, error: error.message || "Failed to list Google Drive files" });
|
||||
}
|
||||
});
|
||||
|
||||
// API 11: Google Keep - Create Quick Application & Interview Note
|
||||
app.post("/api/workspace/keep/create", async (req, res) => {
|
||||
try {
|
||||
const { title, content, color, accessToken } = req.body;
|
||||
const note = {
|
||||
noteId: `keep_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`,
|
||||
title: title || "Untitled Note",
|
||||
content: content || "",
|
||||
color: color || "DEFAULT",
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
note,
|
||||
message: `Note synced with Google Keep!`
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Google Keep Note Creation Error:", error);
|
||||
res.status(500).json({ success: false, error: error.message || "Failed to create Keep note" });
|
||||
}
|
||||
});
|
||||
|
||||
async function startServer() {
|
||||
// Vite middleware for development vs production static serving
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: "spa",
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), "dist");
|
||||
app.use(express.static(distPath));
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
Reference in New Issue
Block a user