mgbam commited on
Commit
d5db0b0
·
verified ·
1 Parent(s): b35b579

Upload 14 files

Browse files
Files changed (14) hide show
  1. .env.local +1 -0
  2. .gitignore +24 -0
  3. App.tsx +200 -0
  4. README.md +77 -9
  5. context.ts +0 -0
  6. index.css +496 -0
  7. index.html +36 -0
  8. index.tsx +19 -0
  9. main.py +1 -0
  10. metadata.json +6 -0
  11. package.json +23 -0
  12. requirements.txt +5 -0
  13. tsconfig.json +30 -0
  14. 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
- title: Ai Resume Builder
3
- emoji: 🐢
4
- colorFrom: green
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 5.34.2
8
- app_file: app.py
9
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
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
+ });