diff --git a/nextstep-backend/assets/resume-templates/Basic professional resume.docx b/nextstep-backend/assets/resume-templates/Basic professional resume.docx new file mode 100644 index 0000000..afbf26d Binary files /dev/null and b/nextstep-backend/assets/resume-templates/Basic professional resume.docx differ diff --git a/nextstep-backend/assets/resume-templates/Clean elegant resume.docx b/nextstep-backend/assets/resume-templates/Clean elegant resume.docx new file mode 100644 index 0000000..6f77e20 Binary files /dev/null and b/nextstep-backend/assets/resume-templates/Clean elegant resume.docx differ diff --git a/nextstep-backend/assets/resume-templates/Impact resume.docx b/nextstep-backend/assets/resume-templates/Impact resume.docx new file mode 100644 index 0000000..20e43ac Binary files /dev/null and b/nextstep-backend/assets/resume-templates/Impact resume.docx differ diff --git a/nextstep-backend/assets/resume-templates/Modern multi-page resume.docx b/nextstep-backend/assets/resume-templates/Modern multi-page resume.docx new file mode 100644 index 0000000..903db15 Binary files /dev/null and b/nextstep-backend/assets/resume-templates/Modern multi-page resume.docx differ diff --git a/nextstep-backend/assets/resume-templates/Resume chronological.docx b/nextstep-backend/assets/resume-templates/Resume chronological.docx new file mode 100644 index 0000000..2bca29c Binary files /dev/null and b/nextstep-backend/assets/resume-templates/Resume chronological.docx differ diff --git a/nextstep-backend/package.json b/nextstep-backend/package.json index 67d1f63..9d37754 100644 --- a/nextstep-backend/package.json +++ b/nextstep-backend/package.json @@ -20,7 +20,10 @@ }, "homepage": "https://github.com/NextStepFinalProject/NextStep#readme", "dependencies": { + "@types/adm-zip": "^0.5.7", "@types/pdf-parse": "^1.1.5", + "@types/xmldom": "^0.1.34", + "adm-zip": "^0.5.16", "axios": "^1.8.3", "bcrypt": "^5.1.1", "body-parser": "^1.20.3", @@ -44,7 +47,8 @@ "socket.io": "^4.8.1", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1" + "swagger-ui-express": "^5.0.1", + "xmldom": "^0.6.0" }, "devDependencies": { "@babel/preset-env": "^7.26.0", diff --git a/nextstep-backend/src/config/config.ts b/nextstep-backend/src/config/config.ts index cda46c2..40087a1 100644 --- a/nextstep-backend/src/config/config.ts +++ b/nextstep-backend/src/config/config.ts @@ -20,6 +20,9 @@ export const config = { resumesDirectoryPath: () => 'resources/resumes', resumeMaxSize: () => 5 * 1024 * 1024 // Max file size: 5MB }, + assets: { + resumeTemplatesDirectoryPath: () => 'assets/resume-templates', + }, chatAi: { api_url: () => process.env.CHAT_AI_API_URL || 'https://openrouter.ai/api/v1/chat/completions', api_key: () => process.env.OPENROUTER_API_KEY || undefined, diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts index 3b6d70f..4ef1d63 100644 --- a/nextstep-backend/src/controllers/resume_controller.ts +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { config } from '../config/config'; import fs from 'fs'; import path from 'path'; -import { scoreResume, streamScoreResume } from '../services/resume_service'; +import { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume } from '../services/resume_service'; import multer from 'multer'; import { CustomRequest } from "types/customRequest"; import { handleError } from "../utils/handle_error"; @@ -69,7 +69,28 @@ const getStreamResumeScore = async (req: Request, res: Response) => { } }; -export default { - getResumeScore, - getStreamResumeScore -}; \ No newline at end of file +const getTemplates = async (req: Request, res: Response) => { + try { + const templates = await getResumeTemplates(); + return res.status(200).json(templates); + } catch (error) { + handleError(error, res); + } +}; + +const generateResume = async (req: Request, res: Response) => { + try { + const { feedback, jobDescription, templateName } = req.body; + + if (!feedback || !jobDescription || !templateName) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + const result = await generateImprovedResume(feedback, jobDescription, templateName); + return res.status(200).json(result); + } catch (error) { + handleError(error, res); + } +}; + +export default { getResumeScore, getStreamResumeScore, getTemplates, generateResume }; \ No newline at end of file diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index 5b51507..f724bcd 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -952,6 +952,110 @@ paths: '500': description: Internal server error + /resume/templates: + get: + tags: + - Resume + summary: Get available resume templates + security: + - BearerAuth: [] + responses: + '200': + description: List of available resume templates retrieved successfully + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + description: The name of the template (with extension) + content: + type: string + description: Base64 encoded content of the template file + type: + type: string + description: MIME type of the template file + enum: + - application/pdf + - application/msword + - application/vnd.openxmlformats-officedocument.wordprocessingml.document + '401': + description: Unauthorized + '500': + description: Internal server error + + /resume/generate: + post: + tags: + - Resume + summary: Generate an improved resume based on a resume template, feedback and job description + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - feedback + - jobDescription + - templateName + properties: + feedback: + type: string + description: The feedback received from the resume scoring process + jobDescription: + type: string + description: The job description used for scoring the resume + templateName: + type: string + description: The name of the template to use for generating the improved resume + responses: + '200': + description: The generated improved resume + content: + application/json: + schema: + type: object + properties: + content: + type: string + description: Base64 encoded content of the generated resume file + type: + type: string + description: MIME type of the generated resume file + enum: + - application/msword + - application/vnd.openxmlformats-officedocument.wordprocessingml.document + '400': + description: Bad request - Missing required fields + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message indicating missing fields + '401': + description: Unauthorized - Missing or invalid authentication token + '404': + description: Template not found + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message indicating template not found + '500': + description: Internal server error + /room/user/{receiverUserId}: get: tags: diff --git a/nextstep-backend/src/routes/resume_routes.ts b/nextstep-backend/src/routes/resume_routes.ts index 92b4611..017b26a 100644 --- a/nextstep-backend/src/routes/resume_routes.ts +++ b/nextstep-backend/src/routes/resume_routes.ts @@ -8,4 +8,8 @@ router.get('/score/:filename', Resume.getResumeScore); router.get('/streamScore/:filename', Resume.getStreamResumeScore); +router.get('/templates', Resume.getTemplates); + +router.post('/generate', Resume.generateResume); + export default router; \ No newline at end of file diff --git a/nextstep-backend/src/services/chat_api_service.ts b/nextstep-backend/src/services/chat_api_service.ts index a21eae8..1b5a941 100644 --- a/nextstep-backend/src/services/chat_api_service.ts +++ b/nextstep-backend/src/services/chat_api_service.ts @@ -8,7 +8,7 @@ const cleanResponse = (response: string): string => { /** * @example * await streamChatWithAI( - * "How would you build the tallest building ever?", + * ["How would you build the tallest building ever?"], * "You are a helpful assistant.", * (chunk) => { * // Handle each chunk of the response @@ -18,8 +18,8 @@ const cleanResponse = (response: string): string => { * ); */ export const streamChatWithAI = async ( - userMessageContent: string, systemMessageContent: string, + userMessageContents: string[], onChunk: (chunk: string) => void ): Promise => { try { @@ -32,11 +32,6 @@ export const streamChatWithAI = async ( content: systemMessageContent }; - const userMessage = { - role: 'user', - content: userMessageContent - }; - const response = await fetch(API_URL, { method: 'POST', headers: { @@ -45,7 +40,10 @@ export const streamChatWithAI = async ( }, body: JSON.stringify({ model: MODEL_NAME, - messages: [systemMessage, userMessage], + messages: [ systemMessage, ...userMessageContents.map(userMessageContent => ({ + role: 'user', + content: userMessageContent + })) ], stream: true, }), }); @@ -99,7 +97,7 @@ export const streamChatWithAI = async ( } } -export const chatWithAI = async (userMessageContent: string, systemMessageContent: string): Promise => { +export const chatWithAI = async (systemMessageContent: string, userMessageContents: string[]): Promise => { try { const API_URL = config.chatAi.api_url(); const API_KEY = config.chatAi.api_key(); @@ -110,16 +108,14 @@ export const chatWithAI = async (userMessageContent: string, systemMessageConten content: systemMessageContent }; - const userMessage = { - role: 'user', - content: userMessageContent - }; - const response = await axios.post( API_URL, { model: MODEL_NAME, - messages: [ systemMessage, userMessage ], + messages: [ systemMessage, ...userMessageContents.map(userMessageContent => ({ + role: 'user', + content: userMessageContent + })) ], }, { headers: { diff --git a/nextstep-backend/src/services/posts_service.ts b/nextstep-backend/src/services/posts_service.ts index 643cb97..b8998ce 100644 --- a/nextstep-backend/src/services/posts_service.ts +++ b/nextstep-backend/src/services/posts_service.ts @@ -32,7 +32,7 @@ const addMisterAIComment = async (postId: string, postContent: string) => { misterAI = await usersService.addUser('misterai', 'securepassword', 'misterai@example.com', 'local'); } - const comment = await chatService.chatWithAI(postContent, 'You are an AI assistant tasked with providing the first comment on forum posts. Your responses should be relevant, engaging, and encourage further discussion, also must be short, and you must answer if you know the answer. Ensure your comments are appropriate for the content and tone of the post. Also must answer in the language of the user post. answer short answers. dont ask questions to follow up'); + const comment = await chatService.chatWithAI('You are an AI assistant tasked with providing the first comment on forum posts. Your responses should be relevant, engaging, and encourage further discussion, also must be short, and you must answer if you know the answer. Ensure your comments are appropriate for the content and tone of the post. Also must answer in the language of the user post. answer short answers. dont ask questions to follow up', [postContent]); const commentData: CommentData = { postId, owner: misterAI.id, content: comment }; const savedComment = await commentsService.addComment(commentData); return savedComment; diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index 5fbbdfd..f5ce97e 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -5,8 +5,10 @@ import axios from 'axios'; import { chatWithAI, streamChatWithAI } from './chat_api_service'; import mammoth from 'mammoth'; import pdfParse from 'pdf-parse'; +import AdmZip from 'adm-zip'; +import { DOMParser, XMLSerializer } from 'xmldom'; -const SYSTEM_TEMPLATE = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named BOb the Resume builder. +const SYSTEM_TEMPLATE = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named Bob the Resume builder. You will review resumes with or without job descriptions. You are an expert in resume evaluation and provide constructive feedback with dynamic evaluation. You should also provide an improvement table, taking into account: @@ -102,7 +104,7 @@ const scoreResume = async (resumePath: string, jobDescription?: string): Promise let feedback = FEEDBACK_ERROR_MESSAGE; if (config.chatAi.turned_on()) { // Get feedback from the AI - feedback = await chatWithAI(prompt, SYSTEM_TEMPLATE); + feedback = await chatWithAI(SYSTEM_TEMPLATE, [prompt]); } // Extract the score from the feedback @@ -137,8 +139,8 @@ const streamScoreResume = async ( if (config.chatAi.turned_on()) { await streamChatWithAI( - prompt, SYSTEM_TEMPLATE, + [prompt], (chunk) => { fullResponse += chunk; onChunk(chunk); @@ -166,4 +168,222 @@ const streamScoreResume = async ( } }; -export { scoreResume, streamScoreResume }; \ No newline at end of file +const getResumeTemplates = async (): Promise<{ name: string; content: string; type: string }[]> => { + try { + const templatesDir = config.assets.resumeTemplatesDirectoryPath(); + if (!fs.existsSync(templatesDir)) { + return []; + } + + const files = fs.readdirSync(templatesDir); + const templates = await Promise.all( + files + .filter(file => { + const ext = path.extname(file).toLowerCase(); + return ['.pdf', '.doc', '.docx'].includes(ext); + }) + .map(async file => { + const filePath = path.join(templatesDir, file); + const content = fs.readFileSync(filePath); + const base64Content = content.toString('base64'); + const mimeType = { + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }[path.extname(file).toLowerCase()] || 'application/octet-stream'; + + return { + name: path.basename(file), + content: base64Content, + type: mimeType + }; + }) + ); + + return templates; + } catch (error) { + console.error('Error reading resume templates:', error); + return []; + } +}; + +// Helper to split a string into N parts +function splitString(str: string, parts: number): string[] { + const len = Math.ceil(str.length / parts); + return Array.from({ length: parts }, (_, i) => str.slice(i * len, (i + 1) * len)); +} + +const generateImprovedResume = async ( + feedback: string, + jobDescription: string, + templateName: string +): Promise<{ content: string; type: string }> => { + try { + const templatesDir = config.assets.resumeTemplatesDirectoryPath(); + const templatePath = path.join(templatesDir, templateName); + + if (!fs.existsSync(templatePath)) { + throw new Error(`Template ${templateName} not found`); + } + + // Only handle DOCX files for now + if (!templateName.toLowerCase().endsWith('.docx')) { + throw new Error('Only DOCX templates are currently supported'); + } + + // Read and unzip the DOCX template + const zip = new AdmZip(templatePath); + const documentXml = zip.getEntry('word/document.xml'); + if (!documentXml) { + throw new Error('Could not find document.xml in the template'); + } + + // Parse the XML content + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(documentXml.getData().toString(), 'text/xml'); + + // Extract the content structure with full XML context + const paragraphs = xmlDoc.getElementsByTagName('w:p'); + const contentStructure = []; + + for (let i = 0; i < paragraphs.length; i++) { + const paragraph = paragraphs[i]; + const runs = paragraph.getElementsByTagName('w:r'); + const paragraphContent = []; + + // Get paragraph properties + const pPr = paragraph.getElementsByTagName('w:pPr')[0]; + const paragraphStyle = pPr ? pPr.toString().replace(/"/g, '\\"') : ''; + + for (let j = 0; j < runs.length; j++) { + const run = runs[j]; + const text = run.getElementsByTagName('w:t')[0]; + if (text) { + // Get run properties + const rPr = run.getElementsByTagName('w:rPr')[0]; + const runStyle = rPr ? rPr.toString().replace(/"/g, '\\"') : ''; + + paragraphContent.push({ + text: text.textContent, + style: runStyle + }); + } + } + + if (paragraphContent.length > 0) { + contentStructure.push({ + type: 'paragraph', + content: paragraphContent, + style: paragraphStyle + }); + } + } + + // Convert structure to readable text for AI while preserving context + const readableContent = contentStructure.map((para, index) => { + const content = para.content.map(run => run.text).join(''); + return `[Paragraph ${index + 1}] +Content: ${content}`; + }).join('\n\n'); + + // Prepare the prompt for AI to modify the content + const prompt = `You are a resume expert. Please modify the following resume content based on the feedback and job description. + +Current Resume Content: +${readableContent} + +Feedback: +${feedback} + +Job Description: +${jobDescription} + +IMPORTANT: You must return your response in the following EXACT JSON format. Do not include any other text or explanation: + +[ + { + "paragraphIndex": 0, + "text": "First paragraph content here" + } +] + +Rules: +1. Return ONLY the JSON array, nothing else +2. Each paragraph must maintain its original structure +3. The text content should be updated based on the feedback while preserving formatting +4. Maintain the same number of paragraphs as the original +5. Do not include any markdown, formatting, or additional text`; + + // Get the modified content from AI + const modifiedContent = await chatWithAI(SYSTEM_TEMPLATE, [prompt]); + console.log('AI Response:', modifiedContent); // Debug log + + let modifiedParagraphs; + try { + // Clean the response to ensure it's valid JSON + const cleanedResponse = modifiedContent.trim() + .replace(/^```json\s*/, '') + .replace(/```\s*$/, '') + .replace(/^\[/, '[') + .replace(/\]$/, ']') + .replace(/\n/g, ' ') // Remove newlines that might break JSON + .replace(/\r/g, '') // Remove carriage returns + .replace(/\t/g, ' ') // Replace tabs with spaces + .replace(/\s+/g, ' '); // Normalize whitespace + + modifiedParagraphs = JSON.parse(cleanedResponse); + + // Validate the structure + if (!Array.isArray(modifiedParagraphs)) { + throw new Error('Response is not an array'); + } + + for (const para of modifiedParagraphs) { + if (!para.text || typeof para.text !== 'string') { + throw new Error('Invalid paragraph structure: missing or invalid text property'); + } + } + + } catch (error: any) { + console.error('Error parsing AI response:', error); + console.error('Raw AI response:', modifiedContent); + throw new Error(`Failed to parse AI response: ${error.message}`); + } + + // Update the document with modified content while preserving structure + for (let i = 0; i < paragraphs.length && i < modifiedParagraphs.length; i++) { + const paragraph = paragraphs[i]; + const modifiedParagraph = modifiedParagraphs[i]; + const runs = paragraph.getElementsByTagName('w:r'); + + // Update the first run's text content while preserving its style + if (runs.length > 0) { + const firstRun = runs[0]; + const text = firstRun.getElementsByTagName('w:t')[0]; + if (text) { + text.textContent = modifiedParagraph.text; + } + } + } + + // Serialize the modified XML + const serializer = new XMLSerializer(); + const modifiedXml = serializer.serializeToString(xmlDoc); + + // Update the document.xml in the zip + zip.updateFile('word/document.xml', Buffer.from(modifiedXml)); + + // Get the modified DOCX as a buffer + const modifiedDocxBuffer = zip.toBuffer(); + + return { + content: modifiedDocxBuffer.toString('base64'), + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }; + } catch (error: any) { + console.error('Error generating improved resume:', error); + throw error; + } +}; + +export { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume }; \ No newline at end of file diff --git a/nextstep-frontend/src/assets/defaultProfileImage.jpg b/nextstep-frontend/assets/defaultProfileImage.jpg similarity index 100% rename from nextstep-frontend/src/assets/defaultProfileImage.jpg rename to nextstep-frontend/assets/defaultProfileImage.jpg diff --git a/nextstep-frontend/package.json b/nextstep-frontend/package.json index 3f86cd3..603e350 100644 --- a/nextstep-frontend/package.json +++ b/nextstep-frontend/package.json @@ -27,6 +27,7 @@ "react-dom": "^18.3.1", "react-froala-wysiwyg": "^4.5.0", "react-markdown": "^10.1.0", + "react-material-ui-carousel": "^3.4.2", "react-router-dom": "^7.1.5", "remark-gfm": "^4.0.1", "socket.io-client": "^4.8.1" diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index 58f988e..4eb7bc9 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -19,15 +19,15 @@ const App: React.FC = () => { <> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/nextstep-frontend/src/assets/react.svg b/nextstep-frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/nextstep-frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/nextstep-frontend/src/components/Layout.tsx b/nextstep-frontend/src/components/Layout.tsx index 7835387..74d2e6f 100644 --- a/nextstep-frontend/src/components/Layout.tsx +++ b/nextstep-frontend/src/components/Layout.tsx @@ -3,16 +3,19 @@ import React from 'react'; import './Layout.css'; -const Layout: React.FC<{ children: React.ReactNode }> = (props: any) => { +const Layout: React.FC<{ className?: string; children: React.ReactNode }> = ({ + className = '', + children +}) => { return ( - - { props.children } + { children } ); }; diff --git a/nextstep-frontend/src/pages/Dashboard.tsx b/nextstep-frontend/src/pages/Dashboard.tsx index 1339560..93ad626 100644 --- a/nextstep-frontend/src/pages/Dashboard.tsx +++ b/nextstep-frontend/src/pages/Dashboard.tsx @@ -25,7 +25,7 @@ import { ThumbUp, Message, Delete } from '@mui/icons-material'; import { Post } from "../models/Post"; import api from "../serverApi"; import {getUserAuth} from "../handlers/userAuth.ts"; -import defaultProfileImage from '../assets/defaultProfileImage.jpg'; // Import the default profile image +import defaultProfileImage from '../../assets/defaultProfileImage.jpg'; // Import the default profile image const Dashboard: React.FC = () => { diff --git a/nextstep-frontend/src/pages/PostDetails.tsx b/nextstep-frontend/src/pages/PostDetails.tsx index d69ae71..35b965d 100644 --- a/nextstep-frontend/src/pages/PostDetails.tsx +++ b/nextstep-frontend/src/pages/PostDetails.tsx @@ -10,7 +10,7 @@ import { Post as PostModel } from '../models/Post'; import { Comment } from '../models/Comment'; import { getUserAuth } from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; -import defaultProfileImage from '../assets/defaultProfileImage.jpg'; +import defaultProfileImage from '../../assets/defaultProfileImage.jpg'; import { config } from '../config.ts'; const PostDetails: React.FC = () => { diff --git a/nextstep-frontend/src/pages/Profile.tsx b/nextstep-frontend/src/pages/Profile.tsx index 533cf4b..44ee4c1 100644 --- a/nextstep-frontend/src/pages/Profile.tsx +++ b/nextstep-frontend/src/pages/Profile.tsx @@ -4,7 +4,7 @@ import { ExpandMore, ExpandLess } from '@mui/icons-material'; import {getUserAuth} from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; import {UserProfile} from "../models/UserProfile.ts"; -import defaultProfileImage from '../assets/defaultProfileImage.jpg'; +import defaultProfileImage from '../../assets/defaultProfileImage.jpg'; const Profile: React.FC = () => { const [image, setImage] = useState(null); diff --git a/nextstep-frontend/src/pages/Resume.css b/nextstep-frontend/src/pages/Resume.css new file mode 100644 index 0000000..2357c84 --- /dev/null +++ b/nextstep-frontend/src/pages/Resume.css @@ -0,0 +1,3 @@ +.layout-container.resume { + padding-top: 0vh; +} \ No newline at end of file diff --git a/nextstep-frontend/src/pages/Resume.tsx b/nextstep-frontend/src/pages/Resume.tsx index 498e78c..88eee74 100644 --- a/nextstep-frontend/src/pages/Resume.tsx +++ b/nextstep-frontend/src/pages/Resume.tsx @@ -1,11 +1,27 @@ import React, { useState, useRef, useEffect } from 'react'; -import { Box, Button, CircularProgress, Typography, TextField } from '@mui/material'; +import { + Box, + Button, + CircularProgress, + Typography, + TextField, + Stepper, + Step, + StepLabel, + Card, + CardMedia, + CardContent, +} from '@mui/material'; import { styled } from '@mui/material/styles'; import { config } from '../config'; import api from '../serverApi'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import ScoreGauge from '../components/ScoreGauge'; +import Carousel from 'react-material-ui-carousel'; +import './Resume.css'; + +const steps = ['Score your resume', 'Choose a template', 'Generate matching resume']; const UploadBox = styled(Box)(({ theme }) => ({ border: '2px dashed #ccc', @@ -52,31 +68,141 @@ const FeedbackContainer = styled(Box)(({ theme }) => ({ }, })); +const TemplateCard = styled(Card)(({ theme }) => ({ + height: '100%', + display: 'flex', + flexDirection: 'column', + position: 'relative', + width: '100%', + maxWidth: '1200px', + margin: '0 auto', + '& .MuiCardMedia-root': { + height: 'calc(100vh - 300px)', + minHeight: '600px', + backgroundSize: 'contain', + backgroundPosition: 'center', + backgroundColor: theme.palette.grey[100], + width: '100%', + }, +})); + +const NavigationContainer = styled(Box)(({ theme }) => ({ + top: 0, + zIndex: 1000, + backgroundColor: theme.palette.background.default, + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + borderBottom: `1px solid ${theme.palette.divider}`, + marginBottom: theme.spacing(2), +})); + +const StepperContainer = styled(Box)(({ theme }) => ({ + top: 64, + zIndex: 999, + backgroundColor: theme.palette.background.default, + padding: theme.spacing(2, 0), + borderBottom: `1px solid ${theme.palette.divider}`, + marginBottom: theme.spacing(2), +})); + +const GeneratedWordPreview: React.FC<{ base64Content: string }> = ({ base64Content }) => { + const [previewUrl, setPreviewUrl] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const uploadAndPreview = async () => { + setLoading(true); + setError(null); + try { + // Convert base64 to blob + const byteCharacters = atob(base64Content); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); + const formData = new FormData(); + formData.append('file', blob, 'generated.docx'); + const response = await fetch('https://tmpfiles.org/api/v1/upload', { + method: 'POST', + body: formData + }); + if (!response.ok) throw new Error('Failed to upload file'); + const data = await response.json(); + const downloadUrl = data.data.url.replace('https://tmpfiles.org/', 'https://tmpfiles.org/dl/'); + const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(downloadUrl)}`; + setPreviewUrl(officeUrl); + } catch (err: any) { + setError('Failed to prepare document preview'); + } finally { + setLoading(false); + } + }; + uploadAndPreview(); + }, [base64Content]); + + if (loading) return ; + if (error) return {error}; + if (!previewUrl) return null; + return ( + +