From f56668135826ab02dfd5582f2849ba73cb90aecd Mon Sep 17 00:00:00 2001 From: Xitang Date: Mon, 10 Jul 2023 03:23:08 -0700 Subject: [PATCH] Add a returnConcatenatedStringForTextsWithSameHighestScore arg in getTextWithHighestFeatureScore --- package-lock.json | 18 +++--------- package.json | 2 -- src/app/components/ResumeDropzone.tsx | 28 +++++++++++-------- src/app/home/AutoTypingResume.tsx | 2 +- src/app/lib/deep-clone.ts | 8 ++++-- src/app/lib/make-object-char-iterator.ts | 13 ++++++--- .../extract-profile.ts | 4 ++- .../extract-skills.ts | 4 +-- .../lib/feature-scoring-system.ts | 11 ++++++-- src/app/lib/redux/local-storage.ts | 14 ++++------ src/app/resume-import/page.tsx | 4 +-- src/app/resume-parser/ResumeTable.tsx | 10 ++----- 12 files changed, 59 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4efaf51..4cb0ec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "@heroicons/react": "^2.0.18", "@react-pdf/renderer": "^3.1.10", "@reduxjs/toolkit": "^1.9.5", - "@types/lodash": "^4.14.195", "@types/node": "20.2.5", "@types/react": "18.2.7", "@types/react-dom": "18.2.4", @@ -19,7 +18,6 @@ "autoprefixer": "10.4.14", "eslint": "8.41.0", "eslint-config-next": "13.4.4", - "lodash": "^4.17.21", "next": "13.4.4", "pdfjs": "^2.5.0", "pdfjs-dist": "^3.7.107", @@ -1956,11 +1954,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "node_modules/@types/lodash": { - "version": "4.14.195", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", - "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" - }, "node_modules/@types/node": { "version": "20.2.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", @@ -6491,7 +6484,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -10784,11 +10778,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "@types/lodash": { - "version": "4.14.195", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", - "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" - }, "@types/node": { "version": "20.2.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", @@ -14112,7 +14101,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash.merge": { "version": "4.6.2", diff --git a/package.json b/package.json index d11cf62..49258b9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "@heroicons/react": "^2.0.18", "@react-pdf/renderer": "^3.1.10", "@reduxjs/toolkit": "^1.9.5", - "@types/lodash": "^4.14.195", "@types/node": "20.2.5", "@types/react": "18.2.7", "@types/react-dom": "18.2.4", @@ -22,7 +21,6 @@ "autoprefixer": "10.4.14", "eslint": "8.41.0", "eslint-config-next": "13.4.4", - "lodash": "^4.17.21", "next": "13.4.4", "pdfjs": "^2.5.0", "pdfjs-dist": "^3.7.107", diff --git a/src/app/components/ResumeDropzone.tsx b/src/app/components/ResumeDropzone.tsx index 9f53496..2b98485 100644 --- a/src/app/components/ResumeDropzone.tsx +++ b/src/app/components/ResumeDropzone.tsx @@ -3,16 +3,15 @@ import { LockClosedIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/outline"; import { parseResumeFromPdf } from "lib/parse-resume-from-pdf"; import { - isLocalStorageEmpty, + getHasUsedAppBefore, saveStateToLocalStorage, } from "lib/redux/local-storage"; -import { initialSettings } from "lib/redux/settingsSlice"; +import { type ShowForm, initialSettings } from "lib/redux/settingsSlice"; import { useRouter } from "next/navigation"; import addPdfSrc from "public/assets/add-pdf.svg"; import Image from "next/image"; import { cx } from "lib/cx"; -import isEmpty from "lodash/isEmpty"; -import cloneDeep from "lodash/cloneDeep"; +import { deepClone } from "lib/deep-clone"; const defaultFileState = { name: "", @@ -74,15 +73,20 @@ export const ResumeDropzone = ({ const onImportClick = async () => { const resume = await parseResumeFromPdf(file.fileUrl); - let settings = cloneDeep(initialSettings); - const sections = Object.keys( - settings.formToShow - ) as (keyof typeof settings.formToShow)[]; - if (!isLocalStorageEmpty()) { + const settings = deepClone(initialSettings); + + // Set formToShow settings based on uploaded resume if users have used the app before + if (getHasUsedAppBefore()) { + const sections = Object.keys(settings.formToShow) as ShowForm[]; + const sectionToFormToShow: Record = { + workExperiences: resume.workExperiences.length > 0, + educations: resume.educations.length > 0, + projects: resume.projects.length > 0, + skills: resume.skills.descriptions.length > 0, + custom: resume.custom.descriptions.length > 0, + }; for (const section of sections) { - if (isEmpty(resume[section])) { - settings.formToShow[section] = false; - } + settings.formToShow[section] = sectionToFormToShow[section]; } } diff --git a/src/app/home/AutoTypingResume.tsx b/src/app/home/AutoTypingResume.tsx index ef027f0..e9f7955 100644 --- a/src/app/home/AutoTypingResume.tsx +++ b/src/app/home/AutoTypingResume.tsx @@ -21,7 +21,7 @@ const CHARS_PER_INTERVAL = 10; const RESET_INTERVAL_MS = 60 * 1000; // 60s export const AutoTypingResume = () => { - const [resume, setResume] = useState(deepClone(initialResumeState) as Resume); + const [resume, setResume] = useState(deepClone(initialResumeState)); const resumeCharIterator = useRef( makeObjectCharIterator(START_HOME_RESUME, END_HOME_RESUME) ); diff --git a/src/app/lib/deep-clone.ts b/src/app/lib/deep-clone.ts index e7dce0f..9988424 100644 --- a/src/app/lib/deep-clone.ts +++ b/src/app/lib/deep-clone.ts @@ -1,6 +1,8 @@ /** - * Server side deep clone util. + * Server side object deep clone util using JSON serialization. + * Not efficient for large objects but good enough for most use cases. + * * Client side can simply use structuredClone. */ -export const deepClone = (object: { [key: string]: any }) => - JSON.parse(JSON.stringify(object)); +export const deepClone = (object: T) => + JSON.parse(JSON.stringify(object)) as T; diff --git a/src/app/lib/make-object-char-iterator.ts b/src/app/lib/make-object-char-iterator.ts index 137ca93..15f9720 100644 --- a/src/app/lib/make-object-char-iterator.ts +++ b/src/app/lib/make-object-char-iterator.ts @@ -17,8 +17,13 @@ type Object = { [key: string]: any }; * iterator.next().value // {a : "ab"} * iterator.next().value // {a : "abc"} */ -export function* makeObjectCharIterator(start: Object, end: Object, level = 0) { - const object = level === 0 ? deepClone(start) : start; +export function* makeObjectCharIterator( + start: T, + end: T, + level = 0 +) { + // Have to manually cast Object type and return T type due to https://github.com/microsoft/TypeScript/issues/47357 + const object: Object = level === 0 ? deepClone(start) : start; for (const [key, endValue] of Object.entries(end)) { if (typeof endValue === "object") { const recursiveIterator = makeObjectCharIterator( @@ -31,12 +36,12 @@ export function* makeObjectCharIterator(start: Object, end: Object, level = 0) { if (next.done) { break; } - yield deepClone(object); + yield deepClone(object) as T; } } else { for (let i = 1; i <= endValue.length; i++) { object[key] = endValue.slice(0, i); - yield deepClone(object); + yield deepClone(object) as T; } } } diff --git a/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-profile.ts b/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-profile.ts index 10dc9d1..8f558f2 100644 --- a/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-profile.ts +++ b/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-profile.ts @@ -148,7 +148,9 @@ export const extractProfile = (sections: ResumeSectionToLines) => { ); const [summary, summaryScores] = getTextWithHighestFeatureScore( textItems, - SUMMARY_FEATURE_SETS + SUMMARY_FEATURE_SETS, + undefined, + true ); const summaryLines = getSectionLinesByKeywords(sections, ["summary"]); diff --git a/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-skills.ts b/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-skills.ts index b0846d9..e551f0d 100644 --- a/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-skills.ts +++ b/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/extract-skills.ts @@ -1,4 +1,4 @@ -import type { FeaturedSkill, ResumeSkills } from "lib/redux/types"; +import type { ResumeSkills } from "lib/redux/types"; import type { ResumeSectionToLines } from "lib/parse-resume-from-pdf/types"; import { deepClone } from "lib/deep-clone"; import { getSectionLinesByKeywords } from "lib/parse-resume-from-pdf/extract-resume-from-sections/lib/get-section-lines"; @@ -14,7 +14,7 @@ export const extractSkills = (sections: ResumeSectionToLines) => { const descriptionsLines = lines.slice(descriptionsLineIdx); const descriptions = getBulletPointsFromLines(descriptionsLines); - const featuredSkills = deepClone(initialFeaturedSkills) as FeaturedSkill[]; + const featuredSkills = deepClone(initialFeaturedSkills); if (descriptionsLineIdx !== 0) { const featuredSkillsLines = lines.slice(0, descriptionsLineIdx); const featuredSkillsTextItems = featuredSkillsLines diff --git a/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/lib/feature-scoring-system.ts b/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/lib/feature-scoring-system.ts index ad2fb32..38a3abf 100644 --- a/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/lib/feature-scoring-system.ts +++ b/src/app/lib/parse-resume-from-pdf/extract-resume-from-sections/lib/feature-scoring-system.ts @@ -50,7 +50,8 @@ const computeFeatureScores = ( export const getTextWithHighestFeatureScore = ( textItems: TextItems, featureSets: FeatureSet[], - returnEmptyStringIfHighestScoreIsNotPositive = true + returnEmptyStringIfHighestScoreIsNotPositive = true, + returnConcatenatedStringForTextsWithSameHighestScore = false ) => { const textScores = computeFeatureScores(textItems, featureSets); @@ -61,7 +62,7 @@ export const getTextWithHighestFeatureScore = ( if (score > highestScore) { textsWithHighestFeatureScore = []; } - textsWithHighestFeatureScore.push(text.trim()); + textsWithHighestFeatureScore.push(text); highestScore = score; } } @@ -69,5 +70,9 @@ export const getTextWithHighestFeatureScore = ( if (returnEmptyStringIfHighestScoreIsNotPositive && highestScore <= 0) return ["", textScores] as const; - return [textsWithHighestFeatureScore.join(" "), textScores] as const; + const text = !returnConcatenatedStringForTextsWithSameHighestScore + ? textsWithHighestFeatureScore[0] + : textsWithHighestFeatureScore.map((s) => s.trim()).join(" "); + + return [text, textScores] as const; }; diff --git a/src/app/lib/redux/local-storage.ts b/src/app/lib/redux/local-storage.ts index 35c9a03..d365e4d 100644 --- a/src/app/lib/redux/local-storage.ts +++ b/src/app/lib/redux/local-storage.ts @@ -4,7 +4,7 @@ import type { RootState } from "lib/redux/store"; const LOCAL_STORAGE_KEY = "open-resume-state"; -export function loadStateFromLocalStorage() { +export const loadStateFromLocalStorage = () => { try { const stringifiedState = localStorage.getItem(LOCAL_STORAGE_KEY); if (!stringifiedState) return undefined; @@ -12,17 +12,15 @@ export function loadStateFromLocalStorage() { } catch (e) { return undefined; } -} +}; -export function isLocalStorageEmpty() { - return !localStorage.length; -} - -export function saveStateToLocalStorage(state: RootState) { +export const saveStateToLocalStorage = (state: RootState) => { try { const stringifiedState = JSON.stringify(state); localStorage.setItem(LOCAL_STORAGE_KEY, stringifiedState); } catch (e) { // Ignore } -} +}; + +export const getHasUsedAppBefore = () => Boolean(loadStateFromLocalStorage()); diff --git a/src/app/resume-import/page.tsx b/src/app/resume-import/page.tsx index 81ee00a..251f7df 100644 --- a/src/app/resume-import/page.tsx +++ b/src/app/resume-import/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { loadStateFromLocalStorage } from "lib/redux/local-storage"; +import { getHasUsedAppBefore } from "lib/redux/local-storage"; import { ResumeDropzone } from "components/ResumeDropzone"; import { useState, useEffect } from "react"; import Link from "next/link"; @@ -12,7 +12,7 @@ export default function ImportResume() { }; useEffect(() => { - setHasUsedAppBefore(Boolean(loadStateFromLocalStorage())); + setHasUsedAppBefore(getHasUsedAppBefore()); }, []); return ( diff --git a/src/app/resume-parser/ResumeTable.tsx b/src/app/resume-parser/ResumeTable.tsx index 8edb1c9..b0d1d42 100644 --- a/src/app/resume-parser/ResumeTable.tsx +++ b/src/app/resume-parser/ResumeTable.tsx @@ -1,9 +1,5 @@ import { Fragment } from "react"; -import type { - Resume, - ResumeEducation, - ResumeWorkExperience, -} from "lib/redux/types"; +import type { Resume } from "lib/redux/types"; import { initialEducation, initialWorkExperience } from "lib/redux/resumeSlice"; import { deepClone } from "lib/deep-clone"; import { cx } from "lib/cx"; @@ -45,11 +41,11 @@ const TableRow = ({ export const ResumeTable = ({ resume }: { resume: Resume }) => { const educations = resume.educations.length === 0 - ? [deepClone(initialEducation) as ResumeEducation] + ? [deepClone(initialEducation)] : resume.educations; const workExperiences = resume.workExperiences.length === 0 - ? [deepClone(initialWorkExperience) as ResumeWorkExperience] + ? [deepClone(initialWorkExperience)] : resume.workExperiences; const skills = [...resume.skills.descriptions]; const featuredSkills = resume.skills.featuredSkills