mgbam commited on
Commit
2e9c97f
·
verified ·
1 Parent(s): 9186bfb

Upload 11 files

Browse files
components/AiCoPilotPanel.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import React from 'react';
6
+ import type { ActiveEditor } from '../lib/types';
7
+
8
+ interface AiCoPilotPanelProps {
9
+ activeEditor: ActiveEditor | null;
10
+ onRefine: (instruction: string) => void;
11
+ isLoading: boolean;
12
+ }
13
+
14
+ export default function AiCoPilotPanel({ activeEditor, onRefine, isLoading }: AiCoPilotPanelProps) {
15
+ if (!activeEditor) {
16
+ return (
17
+ <div className="ai-copilot-panel">
18
+ <div className="copilot-placeholder">
19
+ <i className="ph-cursor-text" style={{fontSize: '2rem', marginBottom: '0.5rem'}}></i>
20
+ Click on a section in your resume to activate the AI Co-pilot.
21
+ </div>
22
+ </div>
23
+ );
24
+ }
25
+
26
+ const refinementActions = [
27
+ { instruction: "Rewrite this section to be more impactful.", label: "Rewrite for Impact", icon: "ph-shooting-star" },
28
+ { instruction: "Make this section more concise.", label: "Make More Concise", icon: "ph-arrows-in-simple" },
29
+ { instruction: "Rephrase this using stronger, professional action verbs.", label: "Use Stronger Verbs", icon: "ph-rocket-launch" },
30
+ { instruction: "Generate 3 alternative bullet points based on this content.", label: "Suggest Bullet Points", icon: "ph-list-bullets" },
31
+ ];
32
+
33
+ return (
34
+ <div className="ai-copilot-panel">
35
+ <h3>AI Co-pilot: <span style={{color: 'var(--color-accent)'}}>{activeEditor.sectionId}</span></h3>
36
+ <div className="copilot-actions">
37
+ {refinementActions.map(({ instruction, label, icon }) => (
38
+ <button
39
+ key={instruction}
40
+ className="copilot-button"
41
+ onClick={() => onRefine(instruction)}
42
+ disabled={isLoading}
43
+ >
44
+ <i className={icon}></i>
45
+ <span>{label}</span>
46
+ {isLoading && <small>(...)</small>}
47
+ </button>
48
+ ))}
49
+ </div>
50
+ </div>
51
+ );
52
+ }
components/ContentContainer.tsx ADDED
File without changes
components/ControlPanel.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import React, { useState } from 'react';
6
+ import InitialInputForm from './InitialInputForm';
7
+ import AiCoPilotPanel from './AiCoPilotPanel';
8
+ import type { InitialInput, ActiveEditor } from '../lib/types';
9
+
10
+ interface ControlPanelProps {
11
+ onGenerate: (formData: InitialInput) => void;
12
+ onClear: () => void;
13
+ activeEditor: ActiveEditor | null;
14
+ onRefine: (instruction: string) => void;
15
+ isGenerating: boolean;
16
+ isRefining: boolean;
17
+ }
18
+
19
+ type Tab = 'setup' | 'copilot';
20
+
21
+ export default function ControlPanel({
22
+ onGenerate,
23
+ onClear,
24
+ activeEditor,
25
+ onRefine,
26
+ isGenerating,
27
+ isRefining
28
+ }: ControlPanelProps) {
29
+ const [activeTab, setActiveTab] = useState<Tab>('setup');
30
+
31
+ return (
32
+ <div className="control-panel">
33
+ <div className="control-panel-tabs">
34
+ <button
35
+ className={`control-panel-tab ${activeTab === 'setup' ? 'active' : ''}`}
36
+ onClick={() => setActiveTab('setup')}
37
+ >
38
+ <i className="ph-note-pencil"></i> Initial Setup
39
+ </button>
40
+ <button
41
+ className={`control-panel-tab ${activeTab === 'copilot' ? 'active' : ''}`}
42
+ onClick={() => setActiveTab('copilot')}
43
+ disabled={!activeEditor}
44
+ aria-disabled={!activeEditor}
45
+ >
46
+ <i className="ph-magic-wand"></i> AI Co-pilot
47
+ </button>
48
+ </div>
49
+
50
+ <div className="tab-content">
51
+ {activeTab === 'setup' && (
52
+ <InitialInputForm
53
+ onSubmit={onGenerate}
54
+ onClear={onClear}
55
+ disabled={isGenerating}
56
+ isLoading={isGenerating}
57
+ />
58
+ )}
59
+ {activeTab === 'copilot' && (
60
+ <AiCoPilotPanel
61
+ activeEditor={activeEditor}
62
+ onRefine={onRefine}
63
+ isLoading={isRefining}
64
+ />
65
+ )}
66
+ </div>
67
+ </div>
68
+ );
69
+ }
components/DesignDisplay.tsx ADDED
File without changes
components/EditableSection.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import React, { useRef, useEffect } from 'react';
6
+ import type { ResumeSection, ResumeSectionType } from '../lib/types';
7
+
8
+ interface EditableSectionProps {
9
+ section: ResumeSection;
10
+ onContentChange: (sectionId: ResumeSectionType, newContent: string) => void;
11
+ isActive: boolean;
12
+ onFocus: (sectionId: ResumeSectionType, content: string) => void;
13
+ onBlur: () => void;
14
+ }
15
+
16
+ export default function EditableSection({
17
+ section,
18
+ onContentChange,
19
+ isActive,
20
+ onFocus,
21
+ onBlur,
22
+ }: EditableSectionProps) {
23
+ const contentRef = useRef<HTMLDivElement>(null);
24
+
25
+ useEffect(() => {
26
+ if (contentRef.current && section.content !== contentRef.current.innerText) {
27
+ contentRef.current.innerText = section.content;
28
+ }
29
+ }, [section.content]);
30
+
31
+ const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
32
+ onContentChange(section.id, e.currentTarget.innerText);
33
+ };
34
+
35
+ return (
36
+ <div
37
+ className={`editable-section ${isActive ? 'active' : ''}`}
38
+ onFocus={() => onFocus(section.id, contentRef.current?.innerText || '')}
39
+ onBlur={onBlur}
40
+ >
41
+ <h2 className="section-title">{section.title}</h2>
42
+ <div
43
+ ref={contentRef}
44
+ className="section-content"
45
+ contentEditable
46
+ suppressContentEditableWarning={true}
47
+ onInput={handleInput}
48
+ aria-label={`${section.title} content`}
49
+ />
50
+ </div>
51
+ );
52
+ }
components/ExampleGallery.tsx ADDED
File without changes
components/FashionInputForm.tsx ADDED
File without changes
components/InitialInputForm.tsx ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import React, { useState, ChangeEvent } from 'react';
6
+ import type { InitialInput } from '../lib/types';
7
+ import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
8
+ import mammoth from 'mammoth';
9
+
10
+ // Set workerSrc for pdf.js. This is crucial for it to work.
11
+ GlobalWorkerOptions.workerSrc = 'pdfjs-dist/build/pdf.worker.min.js';
12
+
13
+ interface InitialInputFormProps {
14
+ onSubmit: (formData: InitialInput) => void;
15
+ onClear: () => void;
16
+ disabled: boolean;
17
+ isLoading: boolean;
18
+ }
19
+
20
+ const initialFormState: InitialInput = {
21
+ fullName: '',
22
+ jobTitle: '',
23
+ careerGoal: '',
24
+ jobDescription: '',
25
+ uploadedResumeContent: '',
26
+ yearsOfExperience: '2-4 Years',
27
+ keySkills: '',
28
+ previousRoles: '',
29
+ education: '',
30
+ };
31
+
32
+ export default function InitialInputForm({ onSubmit, onClear, disabled, isLoading }: InitialInputFormProps) {
33
+ const [formData, setFormData] = useState<InitialInput>(initialFormState);
34
+ const [selectedFileName, setSelectedFileName] = useState<string>('');
35
+ const [fileError, setFileError] = useState<string | null>(null);
36
+
37
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
38
+ const { name, value } = e.target;
39
+ setFormData(prev => ({ ...prev, [name]: value }));
40
+ };
41
+
42
+ const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
43
+ const file = e.target.files?.[0];
44
+ setFileError(null);
45
+ setSelectedFileName('');
46
+ setFormData(prev => ({ ...prev, uploadedResumeContent: '' }));
47
+
48
+ if (file) {
49
+ setSelectedFileName(file.name);
50
+ try {
51
+ let text = '';
52
+ if (file.name.toLowerCase().endsWith('.txt')) {
53
+ text = await file.text();
54
+ } else if (file.name.toLowerCase().endsWith('.pdf')) {
55
+ const arrayBuffer = await file.arrayBuffer();
56
+ const pdf = await getDocument({ data: arrayBuffer }).promise;
57
+ let pdfText = '';
58
+ for (let i = 1; i <= pdf.numPages; i++) {
59
+ const page = await pdf.getPage(i);
60
+ const textContent = await page.getTextContent();
61
+ pdfText += textContent.items.map((item: any) => item.str).join(' ') + '\n';
62
+ }
63
+ text = pdfText;
64
+ } else if (file.name.toLowerCase().endsWith('.docx')) {
65
+ const arrayBuffer = await file.arrayBuffer();
66
+ const result = await mammoth.extractRawText({ arrayBuffer });
67
+ text = result.value;
68
+ } else {
69
+ setFileError("Unsupported file type. Please use .txt, .pdf, or .docx.");
70
+ return;
71
+ }
72
+ setFormData(prev => ({ ...prev, uploadedResumeContent: text }));
73
+ } catch (error) {
74
+ setFileError(`Error processing file: ${error instanceof Error ? error.message : "Unknown error"}.`);
75
+ }
76
+ }
77
+ };
78
+
79
+ const handleSubmit = (e: React.FormEvent) => {
80
+ e.preventDefault();
81
+ if (fileError) {
82
+ alert(`Cannot submit: Please resolve the file error first. ${fileError}`);
83
+ return;
84
+ }
85
+ onSubmit(formData);
86
+ };
87
+
88
+ const handleClear = () => {
89
+ setFormData(initialFormState);
90
+ setSelectedFileName('');
91
+ setFileError(null);
92
+ const fileInput = document.getElementById('resumeUpload') as HTMLInputElement;
93
+ if (fileInput) fileInput.value = '';
94
+ onClear();
95
+ };
96
+
97
+ return (
98
+ <form onSubmit={handleSubmit} className="initial-input-form">
99
+ <h2>Initial Setup</h2>
100
+
101
+ {/* Form groups for all inputs */}
102
+ <div className="form-group">
103
+ <label htmlFor="fullName">Full Name</label>
104
+ <input type="text" id="fullName" name="fullName" value={formData.fullName} onChange={handleChange} placeholder="e.g., Jane Doe" required disabled={disabled}/>
105
+ </div>
106
+ <div className="form-group">
107
+ <label htmlFor="jobTitle">Current or Target Job Title</label>
108
+ <input type="text" id="jobTitle" name="jobTitle" value={formData.jobTitle} onChange={handleChange} placeholder="e.g., Senior Software Engineer" required disabled={disabled}/>
109
+ </div>
110
+ <div className="form-group">
111
+ <label htmlFor="jobDescription">Target Job Description (Crucial for Scoring)</label>
112
+ <textarea id="jobDescription" name="jobDescription" value={formData.jobDescription} onChange={handleChange} placeholder="Paste the job description here..." rows={6} disabled={disabled}/>
113
+ </div>
114
+ <div className="form-group">
115
+ <label htmlFor="resumeUpload">Upload Existing Resume (Optional)</label>
116
+ <input type="file" id="resumeUpload" name="resumeUpload" accept=".txt,.pdf,.docx" onChange={handleFileChange} disabled={disabled}/>
117
+ <small className="form-text text-muted">{selectedFileName ? `Selected: ${selectedFileName}` : "Upload for richer context."}</small>
118
+ {fileError && <p role="alert" style={{color:'var(--color-error)',fontSize:'0.9em',marginTop:'0.5em'}}>{fileError}</p>}
119
+ </div>
120
+ <div className="form-group">
121
+ <label htmlFor="keySkills">Key Skills (to supplement resume)</label>
122
+ <textarea id="keySkills" name="keySkills" value={formData.keySkills} onChange={handleChange} placeholder="List skills not detailed in your resume..." rows={3} disabled={disabled}/>
123
+ </div>
124
+ {/* Additional fields like careerGoal, yearsOfExperience etc. can be added here if needed */}
125
+
126
+ <div className="form-actions" style={{marginTop: '1.5rem', display: 'flex', gap: '1rem'}}>
127
+ <button type="submit" className={`submit-button ${isLoading ? 'is-loading' : ''}`} disabled={disabled}>
128
+ <span>{isLoading ? 'Generating...' : 'Generate Resume'}</span>
129
+ </button>
130
+ <button type="button" className="clear-button" onClick={handleClear} disabled={disabled}>
131
+ Clear All
132
+ </button>
133
+ </div>
134
+ </form>
135
+ );
136
+ }
components/JobMatchDashboard.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import React from 'react';
6
+ import LoadingSpinner from './LoadingSpinner';
7
+
8
+ interface JobMatchDashboardProps {
9
+ score: number;
10
+ suggestions: string[];
11
+ isLoading: boolean;
12
+ hasContent: boolean;
13
+ }
14
+
15
+ export default function JobMatchDashboard({ score, suggestions, isLoading, hasContent }: JobMatchDashboardProps) {
16
+ const gaugeRotation = (score / 100) * 180; // 0 score = 0deg, 100 score = 180deg
17
+
18
+ if (!hasContent) {
19
+ return (
20
+ <div className="dashboard">
21
+ <div className="copilot-placeholder">
22
+ <i className="ph-lightbulb" style={{fontSize: '2rem', marginBottom: '0.5rem'}}></i>
23
+ Your Job Match Score and AI Insights will appear here once a resume is generated.
24
+ </div>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ return (
30
+ <div className="dashboard">
31
+ <h2>Live Analysis</h2>
32
+
33
+ <div className="score-gauge">
34
+ <div className="score-gauge-bg"></div>
35
+ <div
36
+ className="score-gauge-fill"
37
+ style={{ '--gauge-rotation': `${gaugeRotation}deg` } as React.CSSProperties}
38
+ ></div>
39
+ <div className="score-text">{score}<span>/100</span></div>
40
+ </div>
41
+
42
+ <div className="suggestions-list">
43
+ <h3>AI Suggestions {isLoading && <small>(Updating...)</small>}</h3>
44
+ {suggestions.length > 0 ? (
45
+ <ul>
46
+ {suggestions.map((suggestion, index) => (
47
+ <li key={index} className="suggestion-item">
48
+ <i className="ph-check-circle"></i>
49
+ <span>{suggestion}</span>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ ) : (
54
+ <p>No suggestions at the moment. Looks good!</p>
55
+ )}
56
+ </div>
57
+ </div>
58
+ );
59
+ }
components/LoadingSpinner.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import React from 'react';
6
+
7
+ export default function LoadingSpinner() {
8
+ return (
9
+ <div className="loading-spinner-overlay" role="status" aria-live="polite" aria-label="Loading content">
10
+ <div className="spinner-animation"></div>
11
+ <p>Crafting your content...</p>
12
+ </div>
13
+ );
14
+ }
components/ResumePreview.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import React from 'react';
6
+ import type { ResumeDocument, ResumeSectionType, ActiveEditor } from '../lib/types';
7
+ import EditableSection from './EditableSection';
8
+
9
+ interface ResumePreviewProps {
10
+ document: ResumeDocument;
11
+ onContentChange: (sectionId: ResumeSectionType, newContent: string) => void;
12
+ activeSectionId: ResumeSectionType | undefined;
13
+ onSectionFocus: (editorState: ActiveEditor | null) => void;
14
+ }
15
+
16
+ const ORDERED_SECTION_KEYS: ResumeSectionType[] = ["Professional Summary", "Skills Section", "Experience", "Education"];
17
+
18
+ export default function ResumePreview({
19
+ document,
20
+ onContentChange,
21
+ activeSectionId,
22
+ onSectionFocus
23
+ }: ResumePreviewProps) {
24
+
25
+ const handleFocus = (sectionId: ResumeSectionType, content: string) => {
26
+ onSectionFocus({ sectionId, content });
27
+ };
28
+
29
+ const handleBlur = () => {
30
+ onSectionFocus(null);
31
+ };
32
+
33
+ // Create a map for quick lookup
34
+ const docMap = new Map(document.map(section => [section.id, section]));
35
+
36
+ return (
37
+ <div className="resume-preview-paper" aria-label="Live Resume Preview" tabIndex={-1}>
38
+ {ORDERED_SECTION_KEYS.map((key) => {
39
+ const section = docMap.get(key);
40
+ if (section) {
41
+ return (
42
+ <EditableSection
43
+ key={section.id}
44
+ section={section}
45
+ onContentChange={onContentChange}
46
+ isActive={activeSectionId === section.id}
47
+ onFocus={handleFocus}
48
+ onBlur={handleBlur}
49
+ />
50
+ );
51
+ }
52
+ return null; // Don't render a section if it's not in the document
53
+ })}
54
+ </div>
55
+ );
56
+ }