Spaces:
Running
Running
Upload 14 files
Browse files- .env.local +1 -0
- .gitignore +24 -0
- App.tsx +200 -0
- README.md +77 -9
- context.ts +0 -0
- index.css +496 -0
- index.html +36 -0
- index.tsx +19 -0
- main.py +1 -0
- metadata.json +6 -0
- package.json +23 -0
- requirements.txt +5 -0
- tsconfig.json +30 -0
- vite.config.ts +17 -0
.env.local
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
GEMINI_API_KEY=PLACEHOLDER_API_KEY
|
.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
App.tsx
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* @license
|
3 |
+
* SPDX-License-Identifier: Apache-2.0
|
4 |
+
*/
|
5 |
+
import React, { useState, useEffect, useCallback } from 'react';
|
6 |
+
import ControlPanel from './components/ControlPanel';
|
7 |
+
import ResumePreview from './components/ResumePreview';
|
8 |
+
import JobMatchDashboard from './components/JobMatchDashboard';
|
9 |
+
import LoadingSpinner from './components/LoadingSpinner';
|
10 |
+
import type { InitialInput, ResumeDocument, ResumeSectionType, ScoreResponse, ActiveEditor } from './lib/types';
|
11 |
+
|
12 |
+
// Custom hook for debouncing
|
13 |
+
const useDebounce = (callback: (...args: any[]) => void, delay: number) => {
|
14 |
+
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
|
15 |
+
|
16 |
+
return useCallback((...args: any[]) => {
|
17 |
+
if (timeoutId) {
|
18 |
+
clearTimeout(timeoutId);
|
19 |
+
}
|
20 |
+
const newTimeoutId = setTimeout(() => {
|
21 |
+
callback(...args);
|
22 |
+
}, delay);
|
23 |
+
setTimeoutId(newTimeoutId);
|
24 |
+
}, [callback, delay, timeoutId]);
|
25 |
+
};
|
26 |
+
|
27 |
+
export default function App() {
|
28 |
+
const [jobDescription, setJobDescription] = useState<string>('');
|
29 |
+
const [resumeDocument, setResumeDocument] = useState<ResumeDocument>([]);
|
30 |
+
const [activeEditor, setActiveEditor] = useState<ActiveEditor | null>(null);
|
31 |
+
const [scoreData, setScoreData] = useState<ScoreResponse>({ score: 0, suggestions: [] });
|
32 |
+
|
33 |
+
const [isGenerating, setIsGenerating] = useState(false); // For initial generation
|
34 |
+
const [isScoring, setIsScoring] = useState(false); // For real-time scoring
|
35 |
+
const [isRefining, setIsRefining] = useState(false); // For co-pilot actions
|
36 |
+
const [error, setError] = useState<string | null>(null);
|
37 |
+
|
38 |
+
const parseFullResumeText = (fullText: string): ResumeDocument => {
|
39 |
+
const sections: ResumeDocument = [];
|
40 |
+
const sectionTitles: ResumeSectionType[] = ["Professional Summary", "Skills Section", "Experience", "Education"];
|
41 |
+
const sectionRegex = new RegExp(`##\\s*(${sectionTitles.join('|')})\\s*([\\s\\S]*?)(?=\\n##\\s*(?:${sectionTitles.join('|')})|$)`, 'g');
|
42 |
+
|
43 |
+
let match;
|
44 |
+
while ((match = sectionRegex.exec(fullText)) !== null) {
|
45 |
+
const title = match[1].trim() as ResumeSectionType;
|
46 |
+
const content = match[2].trim();
|
47 |
+
sections.push({ id: title, title, content });
|
48 |
+
}
|
49 |
+
return sections;
|
50 |
+
};
|
51 |
+
|
52 |
+
const handleInitialGenerate = async (input: InitialInput) => {
|
53 |
+
setIsGenerating(true);
|
54 |
+
setError(null);
|
55 |
+
setJobDescription(input.jobDescription || '');
|
56 |
+
setResumeDocument([]);
|
57 |
+
setScoreData({ score: 0, suggestions: [] });
|
58 |
+
|
59 |
+
try {
|
60 |
+
const response = await fetch('http://127.0.0.1:5000/api/generate', {
|
61 |
+
method: 'POST',
|
62 |
+
headers: { 'Content-Type': 'application/json' },
|
63 |
+
body: JSON.stringify(input),
|
64 |
+
});
|
65 |
+
|
66 |
+
if (!response.ok) throw new Error((await response.json()).error || 'Server error');
|
67 |
+
const data = await response.json();
|
68 |
+
const parsedSections = parseFullResumeText(data.resume);
|
69 |
+
setResumeDocument(parsedSections);
|
70 |
+
} catch (err) {
|
71 |
+
handleFetchError(err, "Could not generate resume.");
|
72 |
+
} finally {
|
73 |
+
setIsGenerating(false);
|
74 |
+
}
|
75 |
+
};
|
76 |
+
|
77 |
+
const debouncedScoreRequest = useDebounce(async (doc: ResumeDocument, jd: string) => {
|
78 |
+
if (!jd || doc.length === 0) return;
|
79 |
+
setIsScoring(true);
|
80 |
+
try {
|
81 |
+
const fullText = doc.map(s => `## ${s.title}\n${s.content}`).join('\n\n');
|
82 |
+
const response = await fetch('http://127.0.0.1:5000/api/score', {
|
83 |
+
method: 'POST',
|
84 |
+
headers: { 'Content-Type': 'application/json' },
|
85 |
+
body: JSON.stringify({ resume: fullText, job_description: jd }),
|
86 |
+
});
|
87 |
+
if (!response.ok) return; // Fail silently on scoring
|
88 |
+
const data: ScoreResponse = await response.json();
|
89 |
+
setScoreData(data);
|
90 |
+
} catch (err) {
|
91 |
+
// Don't show scoring errors to user to avoid being noisy
|
92 |
+
console.error("Scoring error:", err);
|
93 |
+
} finally {
|
94 |
+
setIsScoring(false);
|
95 |
+
}
|
96 |
+
}, 1500); // 1.5-second debounce delay
|
97 |
+
|
98 |
+
useEffect(() => {
|
99 |
+
if (resumeDocument.length > 0 && jobDescription) {
|
100 |
+
debouncedScoreRequest(resumeDocument, jobDescription);
|
101 |
+
}
|
102 |
+
}, [resumeDocument, jobDescription, debouncedScoreRequest]);
|
103 |
+
|
104 |
+
const handleContentUpdate = (sectionId: ResumeSectionType, newContent: string) => {
|
105 |
+
setResumeDocument(prevDoc =>
|
106 |
+
prevDoc.map(section =>
|
107 |
+
section.id === sectionId ? { ...section, content: newContent } : section
|
108 |
+
)
|
109 |
+
);
|
110 |
+
};
|
111 |
+
|
112 |
+
const handleRefineSection = async (instruction: string) => {
|
113 |
+
if (!activeEditor) return;
|
114 |
+
setIsRefining(true);
|
115 |
+
setError(null);
|
116 |
+
try {
|
117 |
+
const response = await fetch('http://127.0.0.1:5000/api/refine_section', {
|
118 |
+
method: 'POST',
|
119 |
+
headers: { 'Content-Type': 'application/json' },
|
120 |
+
body: JSON.stringify({
|
121 |
+
text_to_refine: activeEditor.content,
|
122 |
+
instruction: instruction
|
123 |
+
}),
|
124 |
+
});
|
125 |
+
if (!response.ok) throw new Error((await response.json()).error || 'Server error during refinement.');
|
126 |
+
const data = await response.json();
|
127 |
+
handleContentUpdate(activeEditor.sectionId, data.refined_text);
|
128 |
+
} catch (err) {
|
129 |
+
handleFetchError(err, "Could not refine section.");
|
130 |
+
} finally {
|
131 |
+
setIsRefining(false);
|
132 |
+
}
|
133 |
+
};
|
134 |
+
|
135 |
+
const handleFetchError = (err: any, context: string) => {
|
136 |
+
let message = err instanceof Error ? err.message : "An unknown error occurred.";
|
137 |
+
if (message.includes('Failed to fetch')) {
|
138 |
+
message = "Could not connect to the backend server. Is it running? Please start the Python server and try again.";
|
139 |
+
}
|
140 |
+
setError(`${context} ${message}`);
|
141 |
+
};
|
142 |
+
|
143 |
+
const clearAll = () => {
|
144 |
+
setJobDescription('');
|
145 |
+
setResumeDocument([]);
|
146 |
+
setActiveEditor(null);
|
147 |
+
setScoreData({ score: 0, suggestions: [] });
|
148 |
+
setError(null);
|
149 |
+
};
|
150 |
+
|
151 |
+
return (
|
152 |
+
<div className="app-container">
|
153 |
+
<header className="app-header">
|
154 |
+
<h1>AI Resume Studio</h1>
|
155 |
+
<p>Your intelligent co-pilot for crafting the perfect resume.</p>
|
156 |
+
</header>
|
157 |
+
|
158 |
+
{isGenerating && <div className="global-loading-overlay"><LoadingSpinner /></div>}
|
159 |
+
|
160 |
+
{error && <div className="error-message global-error">{error}</div>}
|
161 |
+
|
162 |
+
<div className="app-content-grid">
|
163 |
+
<aside className="control-column">
|
164 |
+
<ControlPanel
|
165 |
+
onGenerate={handleInitialGenerate}
|
166 |
+
onClear={clearAll}
|
167 |
+
activeEditor={activeEditor}
|
168 |
+
onRefine={handleRefineSection}
|
169 |
+
isGenerating={isGenerating}
|
170 |
+
isRefining={isRefining}
|
171 |
+
/>
|
172 |
+
</aside>
|
173 |
+
|
174 |
+
<main className="resume-column">
|
175 |
+
{resumeDocument.length > 0 ? (
|
176 |
+
<ResumePreview
|
177 |
+
document={resumeDocument}
|
178 |
+
onContentChange={handleContentUpdate}
|
179 |
+
activeSectionId={activeEditor?.sectionId}
|
180 |
+
onSectionFocus={setActiveEditor}
|
181 |
+
/>
|
182 |
+
) : (
|
183 |
+
<div className="placeholder-text">
|
184 |
+
<p>Your AI-crafted resume will appear here. Fill in the "Initial Setup" form and click "Generate" to begin!</p>
|
185 |
+
</div>
|
186 |
+
)}
|
187 |
+
</main>
|
188 |
+
|
189 |
+
<aside className="dashboard-column">
|
190 |
+
<JobMatchDashboard
|
191 |
+
score={scoreData.score}
|
192 |
+
suggestions={scoreData.suggestions}
|
193 |
+
isLoading={isScoring}
|
194 |
+
hasContent={resumeDocument.length > 0 && jobDescription.length > 0}
|
195 |
+
/>
|
196 |
+
</aside>
|
197 |
+
</div>
|
198 |
+
</div>
|
199 |
+
);
|
200 |
+
}
|
README.md
CHANGED
@@ -1,12 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
1 |
+
# Interactive AI Resume Studio
|
2 |
+
|
3 |
+
Welcome to the Interactive AI Resume Studio, a professional-grade, full-stack application designed to act as your co-pilot in crafting the perfect resume with the power of Google's Gemini AI.
|
4 |
+
|
5 |
+
This application goes beyond simple generation. It's a live, collaborative environment that provides real-time feedback and powerful AI tools to help you tailor your resume to any job description.
|
6 |
+
|
7 |
+
## Features
|
8 |
+
- **Live Editable Document:** No more separate forms and previews. The resume is your workspace. Click and type directly on the document.
|
9 |
+
- **Real-Time Job Match Scoring:** As you edit, a "Job Match" score instantly updates, showing you how well your resume aligns with the target job description.
|
10 |
+
- **Actionable AI Insights:** Receive a running list of concrete suggestions from the AI, such as keywords to add or achievements to quantify.
|
11 |
+
- **AI Co-pilot Panel:** Select any section of your resume to get contextual actions like "Rewrite," "Make More Concise," or "Generate Alternative Bullet Points."
|
12 |
+
- **Secure and Powerful Backend:** A Python Flask server handles all AI logic, keeping your API key safe and enabling advanced analysis that would be impossible in the browser alone.
|
13 |
+
|
14 |
---
|
15 |
+
|
16 |
+
## 🚀 Getting Started
|
17 |
+
|
18 |
+
Follow these steps to get the application running on your local machine.
|
19 |
+
|
20 |
+
### Prerequisites
|
21 |
+
|
22 |
+
- [Python 3.8+](https://www.python.org/downloads/)
|
23 |
+
- [Node.js](https://nodejs.org/en/) (for potential future frontend build steps)
|
24 |
+
- A Google Gemini API Key. You can get one from [Google AI Studio](https://aistudio.google.com/app/apikey).
|
25 |
+
|
26 |
+
### 1. Backend Setup (Python)
|
27 |
+
|
28 |
+
The backend server is the "brain" of the application.
|
29 |
+
|
30 |
+
**a. Create a Secure Environment File:**
|
31 |
+
|
32 |
+
Create a new file named `.env` in the root of the project directory. This file will hold your secret API key. Copy the contents of `.env.example` into it.
|
33 |
+
|
34 |
+
**`.env` file:**
|
35 |
+
```
|
36 |
+
API_KEY="YOUR_GEMINI_API_KEY_HERE"
|
37 |
+
```
|
38 |
+
Replace `YOUR_GEMINI_API_KEY_HERE` with your actual key.
|
39 |
+
|
40 |
+
**b. Install Python Dependencies:**
|
41 |
+
|
42 |
+
Open your terminal in the project's root directory and run the following command to install the required Python libraries.
|
43 |
+
|
44 |
+
```bash
|
45 |
+
pip install -r requirements.txt
|
46 |
+
```
|
47 |
+
|
48 |
+
**c. Run the Backend Server:**
|
49 |
+
|
50 |
+
Start the Python server with this command:
|
51 |
+
|
52 |
+
```bash
|
53 |
+
python main.py
|
54 |
+
```
|
55 |
+
|
56 |
+
You should see output indicating that the Flask server is running on `http://127.0.0.1:5000`. **Keep this terminal window open.**
|
57 |
+
|
58 |
+
### 2. Frontend Setup (Browser)
|
59 |
+
|
60 |
+
The frontend is a simple HTML file that uses React via an import map.
|
61 |
+
|
62 |
+
**a. Open `index.html` in Your Browser:**
|
63 |
+
|
64 |
+
The easiest way to do this is with a live server extension in your code editor (like "Live Server" in VS Code). This handles CORS issues automatically. Using a live server is **highly recommended.**
|
65 |
+
|
66 |
+
### 3. Use the Application!
|
67 |
+
|
68 |
+
With the backend server running and the frontend open in your browser, you can now use the AI Resume Studio.
|
69 |
+
1. Fill out the "Initial Setup" form.
|
70 |
+
2. Click "Generate Full Resume."
|
71 |
+
3. Once the resume appears, click into any section to start editing. Watch the dashboard on the right update in real-time!
|
72 |
+
4. Use the "AI Co-pilot" tab on the left to refine your sections.
|
73 |
+
|
74 |
---
|
75 |
|
76 |
+
## API Endpoints
|
77 |
+
|
78 |
+
- `POST /api/generate`: Takes initial user data and generates a full resume.
|
79 |
+
- `POST /api/score`: Takes the current resume text and a job description and returns a `{ score, suggestions }` JSON object.
|
80 |
+
- `POST /api/refine_section`: Takes a piece of text and an instruction (e.g., "make it more concise") and returns the refined text.
|
context.ts
ADDED
File without changes
|
index.css
ADDED
@@ -0,0 +1,496 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* @license
|
3 |
+
* SPDX-License-Identifier: Apache-2.0
|
4 |
+
*/
|
5 |
+
|
6 |
+
/* Reset and Global Styles */
|
7 |
+
:root {
|
8 |
+
--font-primary: 'Lato', sans-serif;
|
9 |
+
--font-headings: 'Playfair Display', serif;
|
10 |
+
|
11 |
+
--color-background: #F4F7FC; /* Lighter, more modern background */
|
12 |
+
--color-surface: #FFFFFF;
|
13 |
+
--color-primary-text: #121826; /* Darker for better contrast */
|
14 |
+
--color-secondary-text: #5A6475;
|
15 |
+
--color-accent: #4A6CFF; /* A more modern, vibrant blue accent */
|
16 |
+
--color-accent-dark: #3550D5;
|
17 |
+
--color-accent-light: #E9EDFF; /* Very light for backgrounds */
|
18 |
+
--color-accent-light-translucent: rgba(74, 108, 255, 0.1);
|
19 |
+
--color-border: #E1E6EF;
|
20 |
+
--color-input-bg: #FFFFFF;
|
21 |
+
--color-error: #D93025;
|
22 |
+
--color-error-bg: #FDF0EF;
|
23 |
+
--color-success: #1E8E3E;
|
24 |
+
--color-warning: #F9AB00;
|
25 |
+
|
26 |
+
--border-radius-sm: 6px;
|
27 |
+
--border-radius-md: 10px;
|
28 |
+
--box-shadow-light: 0 2px 4px rgba(18, 24, 38, 0.05);
|
29 |
+
--box-shadow-md: 0 4px 12px rgba(18, 24, 38, 0.08);
|
30 |
+
--box-shadow-focus: 0 0 0 3px rgba(74, 108, 255, 0.25);
|
31 |
+
--transition-speed: 0.25s ease-in-out;
|
32 |
+
--transition-speed-fast: 0.15s ease-in-out;
|
33 |
+
}
|
34 |
+
|
35 |
+
* {
|
36 |
+
box-sizing: border-box;
|
37 |
+
margin: 0;
|
38 |
+
padding: 0;
|
39 |
+
}
|
40 |
+
|
41 |
+
html {
|
42 |
+
font-size: 16px;
|
43 |
+
}
|
44 |
+
|
45 |
+
body {
|
46 |
+
font-family: var(--font-primary);
|
47 |
+
background-color: var(--color-background);
|
48 |
+
color: var(--color-primary-text);
|
49 |
+
line-height: 1.6;
|
50 |
+
}
|
51 |
+
|
52 |
+
#root {
|
53 |
+
width: 100%;
|
54 |
+
}
|
55 |
+
|
56 |
+
/* App Container & Main Layout */
|
57 |
+
.app-container {
|
58 |
+
display: flex;
|
59 |
+
flex-direction: column;
|
60 |
+
height: 100vh;
|
61 |
+
overflow: hidden;
|
62 |
+
}
|
63 |
+
|
64 |
+
.app-header {
|
65 |
+
color: white;
|
66 |
+
padding: 1.5rem 2rem;
|
67 |
+
text-align: left;
|
68 |
+
background: linear-gradient(135deg, #1e3a5f, #2b5797);
|
69 |
+
box-shadow: var(--box-shadow-md);
|
70 |
+
z-index: 100;
|
71 |
+
display: flex;
|
72 |
+
justify-content: space-between;
|
73 |
+
align-items: center;
|
74 |
+
}
|
75 |
+
|
76 |
+
.app-header h1 {
|
77 |
+
font-family: var(--font-headings);
|
78 |
+
font-size: 1.75rem;
|
79 |
+
letter-spacing: 1px;
|
80 |
+
}
|
81 |
+
|
82 |
+
.app-header p {
|
83 |
+
font-size: 0.9rem;
|
84 |
+
opacity: 0.9;
|
85 |
+
font-weight: 300;
|
86 |
+
}
|
87 |
+
|
88 |
+
/* New 3-Column Grid */
|
89 |
+
.app-content-grid {
|
90 |
+
display: grid;
|
91 |
+
grid-template-columns: minmax(350px, 1.2fr) 2fr minmax(320px, 1fr);
|
92 |
+
gap: 1.5rem;
|
93 |
+
padding: 1.5rem;
|
94 |
+
flex-grow: 1;
|
95 |
+
height: calc(100vh - 80px); /* Adjust based on header height */
|
96 |
+
overflow: hidden;
|
97 |
+
}
|
98 |
+
|
99 |
+
.control-column, .resume-column, .dashboard-column {
|
100 |
+
overflow-y: auto;
|
101 |
+
height: 100%;
|
102 |
+
padding-right: 10px; /* For scrollbar */
|
103 |
+
}
|
104 |
+
|
105 |
+
/* Custom Scrollbar */
|
106 |
+
.control-column::-webkit-scrollbar,
|
107 |
+
.resume-column::-webkit-scrollbar,
|
108 |
+
.dashboard-column::-webkit-scrollbar {
|
109 |
+
width: 8px;
|
110 |
+
}
|
111 |
+
.control-column::-webkit-scrollbar-track,
|
112 |
+
.resume-column::-webkit-scrollbar-track,
|
113 |
+
.dashboard-column::-webkit-scrollbar-track {
|
114 |
+
background: #f1f1f1;
|
115 |
+
}
|
116 |
+
.control-column::-webkit-scrollbar-thumb,
|
117 |
+
.resume-column::-webkit-scrollbar-thumb,
|
118 |
+
.dashboard-column::-webkit-scrollbar-thumb {
|
119 |
+
background: #ccc;
|
120 |
+
border-radius: 4px;
|
121 |
+
}
|
122 |
+
.control-column::-webkit-scrollbar-thumb:hover,
|
123 |
+
.resume-column::-webkit-scrollbar-thumb:hover,
|
124 |
+
.dashboard-column::-webkit-scrollbar-thumb:hover {
|
125 |
+
background: #aaa;
|
126 |
+
}
|
127 |
+
|
128 |
+
|
129 |
+
/* Control Panel (Left Column) */
|
130 |
+
.control-panel {
|
131 |
+
background-color: var(--color-surface);
|
132 |
+
border-radius: var(--border-radius-md);
|
133 |
+
box-shadow: var(--box-shadow-light);
|
134 |
+
padding: 1rem;
|
135 |
+
}
|
136 |
+
|
137 |
+
.control-panel-tabs {
|
138 |
+
display: flex;
|
139 |
+
border-bottom: 1px solid var(--color-border);
|
140 |
+
margin-bottom: 1.5rem;
|
141 |
+
}
|
142 |
+
.control-panel-tab {
|
143 |
+
padding: 0.75rem 1rem;
|
144 |
+
cursor: pointer;
|
145 |
+
border: none;
|
146 |
+
background: none;
|
147 |
+
font-size: 1rem;
|
148 |
+
font-weight: 600;
|
149 |
+
color: var(--color-secondary-text);
|
150 |
+
position: relative;
|
151 |
+
transition: color var(--transition-speed-fast);
|
152 |
+
display: flex;
|
153 |
+
align-items: center;
|
154 |
+
gap: 0.5rem;
|
155 |
+
}
|
156 |
+
.control-panel-tab::after {
|
157 |
+
content: '';
|
158 |
+
position: absolute;
|
159 |
+
bottom: -1px;
|
160 |
+
left: 0;
|
161 |
+
width: 100%;
|
162 |
+
height: 3px;
|
163 |
+
background-color: var(--color-accent);
|
164 |
+
transform: scaleX(0);
|
165 |
+
transition: transform var(--transition-speed);
|
166 |
+
}
|
167 |
+
.control-panel-tab.active {
|
168 |
+
color: var(--color-accent);
|
169 |
+
}
|
170 |
+
.control-panel-tab.active::after {
|
171 |
+
transform: scaleX(1);
|
172 |
+
}
|
173 |
+
.control-panel-tab:disabled {
|
174 |
+
color: #ccc;
|
175 |
+
cursor: not-allowed;
|
176 |
+
}
|
177 |
+
.tab-content {
|
178 |
+
animation: fadeIn 0.3s ease;
|
179 |
+
}
|
180 |
+
|
181 |
+
/* Initial Input Form */
|
182 |
+
.initial-input-form h2 {
|
183 |
+
font-family: var(--font-headings);
|
184 |
+
font-size: 1.5rem;
|
185 |
+
color: var(--color-primary-text);
|
186 |
+
margin-bottom: 1.5rem;
|
187 |
+
padding-bottom: 0.5rem;
|
188 |
+
}
|
189 |
+
|
190 |
+
.form-group {
|
191 |
+
margin-bottom: 1.25rem;
|
192 |
+
}
|
193 |
+
|
194 |
+
.form-group label {
|
195 |
+
font-weight: 600;
|
196 |
+
margin-bottom: 0.5rem;
|
197 |
+
color: var(--color-secondary-text);
|
198 |
+
font-size: 0.9rem;
|
199 |
+
display: block;
|
200 |
+
}
|
201 |
+
|
202 |
+
.form-group input,
|
203 |
+
.form-group select,
|
204 |
+
.form-group textarea {
|
205 |
+
width: 100%;
|
206 |
+
padding: 0.75rem 1rem;
|
207 |
+
border: 1px solid var(--color-border);
|
208 |
+
border-radius: var(--border-radius-sm);
|
209 |
+
font-size: 0.95rem;
|
210 |
+
transition: border-color var(--transition-speed), box-shadow var(--transition-speed);
|
211 |
+
}
|
212 |
+
.form-group input:focus,
|
213 |
+
.form-group select:focus,
|
214 |
+
.form-group textarea:focus {
|
215 |
+
outline: none;
|
216 |
+
border-color: var(--color-accent);
|
217 |
+
box-shadow: var(--box-shadow-focus);
|
218 |
+
}
|
219 |
+
.form-group textarea {
|
220 |
+
resize: vertical;
|
221 |
+
min-height: 70px;
|
222 |
+
}
|
223 |
+
.form-group input[type="file"] {
|
224 |
+
padding: 0.5rem;
|
225 |
+
font-size: 0.9rem;
|
226 |
+
}
|
227 |
+
.form-text.text-muted {
|
228 |
+
font-size: 0.8rem;
|
229 |
+
color: #6c757d;
|
230 |
+
margin-top: 0.25rem;
|
231 |
+
}
|
232 |
+
|
233 |
+
/* AI Co-pilot Panel */
|
234 |
+
.ai-copilot-panel h3 {
|
235 |
+
font-family: var(--font-headings);
|
236 |
+
font-size: 1.25rem;
|
237 |
+
margin-bottom: 1rem;
|
238 |
+
}
|
239 |
+
.copilot-placeholder {
|
240 |
+
font-style: italic;
|
241 |
+
color: var(--color-secondary-text);
|
242 |
+
text-align: center;
|
243 |
+
padding: 2rem 1rem;
|
244 |
+
border: 2px dashed var(--color-border);
|
245 |
+
border-radius: var(--border-radius-sm);
|
246 |
+
}
|
247 |
+
.copilot-actions {
|
248 |
+
display: flex;
|
249 |
+
flex-direction: column;
|
250 |
+
gap: 0.75rem;
|
251 |
+
}
|
252 |
+
|
253 |
+
/* Resume Preview (Center Column) - Digital Paper */
|
254 |
+
.resume-preview-paper {
|
255 |
+
background-color: var(--color-surface);
|
256 |
+
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.07);
|
257 |
+
padding: 2.5rem 3rem; /* Generous padding like a real doc */
|
258 |
+
border-radius: 3px;
|
259 |
+
min-height: 100%;
|
260 |
+
outline: none; /* remove focus ring from main container */
|
261 |
+
}
|
262 |
+
|
263 |
+
/* Editable Section */
|
264 |
+
.editable-section {
|
265 |
+
padding: 0.5rem 0;
|
266 |
+
position: relative;
|
267 |
+
margin-bottom: 1.5rem;
|
268 |
+
}
|
269 |
+
.editable-section:focus-within {
|
270 |
+
background-color: var(--color-accent-light-translucent);
|
271 |
+
border-radius: var(--border-radius-sm);
|
272 |
+
}
|
273 |
+
.editable-section.active {
|
274 |
+
background-color: var(--color-accent-light-translucent);
|
275 |
+
box-shadow: inset 3px 0 0 var(--color-accent); /* Left border highlight */
|
276 |
+
padding-left: 10px;
|
277 |
+
margin-left: -10px;
|
278 |
+
}
|
279 |
+
|
280 |
+
.section-title {
|
281 |
+
font-family: var(--font-headings);
|
282 |
+
font-size: 1.5rem;
|
283 |
+
color: var(--color-primary-text);
|
284 |
+
font-weight: 700;
|
285 |
+
margin-bottom: 1rem;
|
286 |
+
padding-bottom: 0.5rem;
|
287 |
+
border-bottom: 2px solid var(--color-primary-text);
|
288 |
+
}
|
289 |
+
.section-content {
|
290 |
+
font-size: 1rem;
|
291 |
+
color: var(--color-secondary-text);
|
292 |
+
line-height: 1.7;
|
293 |
+
white-space: pre-wrap; /* Preserve whitespace and newlines */
|
294 |
+
outline: none; /* No focus outline on the content itself */
|
295 |
+
min-height: 30px; /* ensure there's a clickable area */
|
296 |
+
}
|
297 |
+
.section-content:focus {
|
298 |
+
box-shadow: none; /* ensure no weird focus effects */
|
299 |
+
}
|
300 |
+
.section-content[contenteditable="true"]::before {
|
301 |
+
content: 'Type here or use the AI Co-pilot...';
|
302 |
+
color: #ccc;
|
303 |
+
display: none;
|
304 |
+
font-style: italic;
|
305 |
+
}
|
306 |
+
.section-content[contenteditable="true"]:empty::before {
|
307 |
+
display: block;
|
308 |
+
}
|
309 |
+
|
310 |
+
|
311 |
+
/* Job Match Dashboard (Right Column) */
|
312 |
+
.dashboard {
|
313 |
+
background-color: var(--color-surface);
|
314 |
+
border-radius: var(--border-radius-md);
|
315 |
+
box-shadow: var(--box-shadow-light);
|
316 |
+
padding: 1.5rem;
|
317 |
+
}
|
318 |
+
.dashboard h2 {
|
319 |
+
font-family: var(--font-headings);
|
320 |
+
font-size: 1.5rem;
|
321 |
+
margin-bottom: 1.5rem;
|
322 |
+
}
|
323 |
+
|
324 |
+
/* Score Gauge */
|
325 |
+
.score-gauge {
|
326 |
+
position: relative;
|
327 |
+
width: 150px;
|
328 |
+
height: 75px; /* Half circle */
|
329 |
+
overflow: hidden;
|
330 |
+
margin: 1rem auto;
|
331 |
+
}
|
332 |
+
.score-gauge-bg, .score-gauge-fill {
|
333 |
+
position: absolute;
|
334 |
+
top: 0;
|
335 |
+
left: 0;
|
336 |
+
width: 150px;
|
337 |
+
height: 150px;
|
338 |
+
border-radius: 50%;
|
339 |
+
border: 20px solid;
|
340 |
+
}
|
341 |
+
.score-gauge-bg {
|
342 |
+
border-color: var(--color-border);
|
343 |
+
}
|
344 |
+
.score-gauge-fill {
|
345 |
+
clip: rect(0, 150px, 75px, 0);
|
346 |
+
border-color: var(--color-accent);
|
347 |
+
transform: rotate(var(--gauge-rotation, 0deg));
|
348 |
+
transition: transform 0.5s ease-out;
|
349 |
+
}
|
350 |
+
.score-text {
|
351 |
+
position: absolute;
|
352 |
+
bottom: 0;
|
353 |
+
left: 50%;
|
354 |
+
transform: translateX(-50%);
|
355 |
+
font-size: 2.5rem;
|
356 |
+
font-weight: 700;
|
357 |
+
color: var(--color-primary-text);
|
358 |
+
}
|
359 |
+
.score-text span {
|
360 |
+
font-size: 1.2rem;
|
361 |
+
font-weight: 400;
|
362 |
+
color: var(--color-secondary-text);
|
363 |
+
}
|
364 |
+
|
365 |
+
/* AI Suggestions List */
|
366 |
+
.suggestions-list {
|
367 |
+
list-style: none;
|
368 |
+
padding: 0;
|
369 |
+
margin-top: 2rem;
|
370 |
+
}
|
371 |
+
.suggestions-list h3 {
|
372 |
+
font-size: 1.1rem;
|
373 |
+
font-weight: 700;
|
374 |
+
color: var(--color-secondary-text);
|
375 |
+
margin-bottom: 1rem;
|
376 |
+
border-top: 1px solid var(--color-border);
|
377 |
+
padding-top: 1.5rem;
|
378 |
+
}
|
379 |
+
.suggestion-item {
|
380 |
+
background-color: var(--color-accent-light);
|
381 |
+
padding: 1rem;
|
382 |
+
border-radius: var(--border-radius-sm);
|
383 |
+
margin-bottom: 0.75rem;
|
384 |
+
font-size: 0.9rem;
|
385 |
+
display: flex;
|
386 |
+
align-items: flex-start;
|
387 |
+
gap: 0.75rem;
|
388 |
+
}
|
389 |
+
.suggestion-item i {
|
390 |
+
font-size: 1.2rem;
|
391 |
+
color: var(--color-accent-dark);
|
392 |
+
margin-top: 2px;
|
393 |
+
}
|
394 |
+
|
395 |
+
/* Buttons */
|
396 |
+
.submit-button, .clear-button, .copilot-button {
|
397 |
+
width: 100%;
|
398 |
+
padding: 0.8rem 1.5rem;
|
399 |
+
font-size: 1rem;
|
400 |
+
font-weight: 700;
|
401 |
+
border-radius: var(--border-radius-sm);
|
402 |
+
cursor: pointer;
|
403 |
+
transition: all var(--transition-speed-fast);
|
404 |
+
text-align: center;
|
405 |
+
border: 1px solid transparent;
|
406 |
+
display: flex;
|
407 |
+
justify-content: center;
|
408 |
+
align-items: center;
|
409 |
+
gap: 0.5rem;
|
410 |
+
}
|
411 |
+
|
412 |
+
.submit-button {
|
413 |
+
background-color: var(--color-accent);
|
414 |
+
color: white;
|
415 |
+
border-color: var(--color-accent);
|
416 |
+
}
|
417 |
+
.submit-button:hover:not(:disabled) {
|
418 |
+
background-color: var(--color-accent-dark);
|
419 |
+
}
|
420 |
+
.clear-button {
|
421 |
+
background-color: #F8F9FA;
|
422 |
+
color: var(--color-secondary-text);
|
423 |
+
border-color: var(--color-border);
|
424 |
+
}
|
425 |
+
.clear-button:hover:not(:disabled) {
|
426 |
+
background-color: #f1f3f5;
|
427 |
+
border-color: var(--color-secondary-text);
|
428 |
+
}
|
429 |
+
.copilot-button {
|
430 |
+
background-color: var(--color-accent-light);
|
431 |
+
color: var(--color-accent-dark);
|
432 |
+
justify-content: flex-start;
|
433 |
+
font-weight: 600;
|
434 |
+
}
|
435 |
+
.copilot-button:hover:not(:disabled) {
|
436 |
+
background-color: rgba(74, 108, 255, 0.2);
|
437 |
+
color: var(--color-accent-dark);
|
438 |
+
}
|
439 |
+
|
440 |
+
button:disabled {
|
441 |
+
opacity: 0.6;
|
442 |
+
cursor: not-allowed;
|
443 |
+
}
|
444 |
+
.submit-button.is-loading span { visibility: hidden; }
|
445 |
+
.submit-button.is-loading::after {
|
446 |
+
content: "";
|
447 |
+
position: absolute;
|
448 |
+
width: 20px;
|
449 |
+
height: 20px;
|
450 |
+
border: 2px solid rgba(255, 255, 255, 0.5);
|
451 |
+
border-top-color: #fff;
|
452 |
+
border-radius: 50%;
|
453 |
+
animation: spin 0.8s linear infinite;
|
454 |
+
}
|
455 |
+
@keyframes spin { 100% { transform: rotate(360deg); } }
|
456 |
+
|
457 |
+
/* Global Loading & Error */
|
458 |
+
.global-loading-overlay {
|
459 |
+
position: fixed;
|
460 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
461 |
+
background: rgba(255,255,255,0.7);
|
462 |
+
z-index: 1000;
|
463 |
+
display: flex;
|
464 |
+
justify-content: center;
|
465 |
+
align-items: center;
|
466 |
+
}
|
467 |
+
.error-message {
|
468 |
+
background-color: var(--color-error-bg);
|
469 |
+
color: var(--color-error);
|
470 |
+
border: 1px solid var(--color-error);
|
471 |
+
padding: 1rem 1.5rem;
|
472 |
+
border-radius: var(--border-radius-sm);
|
473 |
+
margin: 1.5rem;
|
474 |
+
box-shadow: var(--box-shadow-light);
|
475 |
+
}
|
476 |
+
|
477 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
478 |
+
|
479 |
+
.placeholder-text {
|
480 |
+
text-align: center;
|
481 |
+
padding: 4rem 2rem;
|
482 |
+
color: var(--color-secondary-text);
|
483 |
+
border: 2px dashed var(--color-border);
|
484 |
+
border-radius: var(--border-radius-md);
|
485 |
+
display: flex;
|
486 |
+
flex-direction: column;
|
487 |
+
justify-content: center;
|
488 |
+
align-items: center;
|
489 |
+
height: 100%;
|
490 |
+
min-height: 250px;
|
491 |
+
}
|
492 |
+
.placeholder-text::before {
|
493 |
+
content: '📄';
|
494 |
+
font-size: 3rem;
|
495 |
+
margin-bottom: 1rem;
|
496 |
+
}
|
index.html
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
<!doctype html>
|
3 |
+
<html lang="en">
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
8 |
+
<title>AI Resume Studio</title>
|
9 |
+
<meta name="description" content="Generate and refine professional resumes with a real-time AI co-pilot, job match scoring, and actionable insights." />
|
10 |
+
<meta name="author" content="AI Assistant" />
|
11 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
12 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
13 |
+
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Playfair+Display:wght@400;500;700&display=swap" rel="stylesheet">
|
14 |
+
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
15 |
+
<script type="importmap">
|
16 |
+
{
|
17 |
+
"imports": {
|
18 |
+
"react": "https://esm.sh/react",
|
19 |
+
"react/": "https://esm.sh/react/",
|
20 |
+
"react-dom": "https://esm.sh/react-dom",
|
21 |
+
"react-dom/": "https://esm.sh/react-dom/",
|
22 |
+
"pdfjs-dist": "https://esm.sh/[email protected]",
|
23 |
+
"pdfjs-dist/build/pdf.worker.min.js": "https://esm.sh/[email protected]/build/pdf.worker.min.js",
|
24 |
+
"mammoth": "https://esm.sh/[email protected]"
|
25 |
+
}
|
26 |
+
}
|
27 |
+
</script>
|
28 |
+
<link rel="stylesheet" href="/index.css">
|
29 |
+
</head>
|
30 |
+
|
31 |
+
<body>
|
32 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
33 |
+
<div id="root"></div>
|
34 |
+
<script type="module" src="/index.tsx"></script>
|
35 |
+
</body>
|
36 |
+
</html>
|
index.tsx
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* @license
|
3 |
+
* SPDX-License-Identifier: Apache-2.0
|
4 |
+
*/
|
5 |
+
import React from 'react';
|
6 |
+
import ReactDOM from 'react-dom/client';
|
7 |
+
import App from './App';
|
8 |
+
|
9 |
+
const rootElement = document.getElementById('root');
|
10 |
+
if (!rootElement) {
|
11 |
+
throw new Error("Could not find root element to mount the application.");
|
12 |
+
}
|
13 |
+
|
14 |
+
const root = ReactDOM.createRoot(rootElement);
|
15 |
+
root.render(
|
16 |
+
<React.StrictMode>
|
17 |
+
<App />
|
18 |
+
</React.StrictMode>
|
19 |
+
);
|
main.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
�jh��,�jh��(� ^�z������� zv�
|
metadata.json
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "AI Resume Builder",
|
3 |
+
"description": "Craft a professional resume with AI-powered suggestions and guidance.",
|
4 |
+
"requestFramePermissions": [],
|
5 |
+
"prompt": ""
|
6 |
+
}
|
package.json
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "ai-resume-builder",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.0",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "vite build",
|
9 |
+
"preview": "vite preview"
|
10 |
+
},
|
11 |
+
"dependencies": {
|
12 |
+
"react": "latest",
|
13 |
+
"react-dom": "latest",
|
14 |
+
"pdfjs-dist": "4.0.379",
|
15 |
+
"pdfjs-dist/build/pdf.worker.min.js": "latest",
|
16 |
+
"mammoth": "1.7.0"
|
17 |
+
},
|
18 |
+
"devDependencies": {
|
19 |
+
"@types/node": "^22.14.0",
|
20 |
+
"typescript": "~5.7.2",
|
21 |
+
"vite": "^6.2.0"
|
22 |
+
}
|
23 |
+
}
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
Flask==2.2.2
|
3 |
+
Flask-Cors==3.0.10
|
4 |
+
google-generativeai==0.7.1
|
5 |
+
python-dotenv==0.21.0
|
tsconfig.json
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"target": "ES2020",
|
4 |
+
"experimentalDecorators": true,
|
5 |
+
"useDefineForClassFields": false,
|
6 |
+
"module": "ESNext",
|
7 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
8 |
+
"skipLibCheck": true,
|
9 |
+
|
10 |
+
/* Bundler mode */
|
11 |
+
"moduleResolution": "bundler",
|
12 |
+
"allowImportingTsExtensions": true,
|
13 |
+
"isolatedModules": true,
|
14 |
+
"moduleDetection": "force",
|
15 |
+
"noEmit": true,
|
16 |
+
"allowJs": true,
|
17 |
+
"jsx": "react-jsx",
|
18 |
+
|
19 |
+
/* Linting */
|
20 |
+
"strict": true,
|
21 |
+
"noUnusedLocals": true,
|
22 |
+
"noUnusedParameters": true,
|
23 |
+
"noFallthroughCasesInSwitch": true,
|
24 |
+
"noUncheckedSideEffectImports": true,
|
25 |
+
|
26 |
+
"paths": {
|
27 |
+
"@/*" : ["./*"]
|
28 |
+
}
|
29 |
+
}
|
30 |
+
}
|
vite.config.ts
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import path from 'path';
|
2 |
+
import { defineConfig, loadEnv } from 'vite';
|
3 |
+
|
4 |
+
export default defineConfig(({ mode }) => {
|
5 |
+
const env = loadEnv(mode, '.', '');
|
6 |
+
return {
|
7 |
+
define: {
|
8 |
+
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
9 |
+
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
10 |
+
},
|
11 |
+
resolve: {
|
12 |
+
alias: {
|
13 |
+
'@': path.resolve(__dirname, '.'),
|
14 |
+
}
|
15 |
+
}
|
16 |
+
};
|
17 |
+
});
|