mirror of
https://github.com/xitanggg/open-resume
synced 2025-02-05 20:12:45 +01:00
added new fonts new color updated the ui of dark theme and chatbot
This commit is contained in:
parent
e3e14c0381
commit
7f574c3a53
22
package-lock.json
generated
22
package-lock.json
generated
@ -21,6 +21,7 @@
|
||||
"docx": "^8.5.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-config-next": "13.4.4",
|
||||
"lucide-react": "^0.451.0",
|
||||
"next": "13.4.4",
|
||||
"open-resume": "file:",
|
||||
"pdfjs": "^2.5.0",
|
||||
@ -8325,6 +8326,14 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.451.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.451.0.tgz",
|
||||
"integrity": "sha512-OwQ3uljZLp2cerj8sboy5rnhtGTCl9UCJIhT1J85/yOuGVlEH+xaUPR7tvNdddPvmV5M5VLdr7cQuWE3hzA4jw==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
@ -17506,6 +17515,12 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"lucide-react": {
|
||||
"version": "0.451.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.451.0.tgz",
|
||||
"integrity": "sha512-OwQ3uljZLp2cerj8sboy5rnhtGTCl9UCJIhT1J85/yOuGVlEH+xaUPR7tvNdddPvmV5M5VLdr7cQuWE3hzA4jw==",
|
||||
"requires": {}
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
@ -17915,6 +17930,7 @@
|
||||
"eslint-config-next": "13.4.4",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"lucide-react": "^0.451.0",
|
||||
"next": "13.4.4",
|
||||
"open-resume": "file:",
|
||||
"pdfjs": "^2.5.0",
|
||||
@ -24229,6 +24245,12 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"lucide-react": {
|
||||
"version": "0.451.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.451.0.tgz",
|
||||
"integrity": "sha512-OwQ3uljZLp2cerj8sboy5rnhtGTCl9UCJIhT1J85/yOuGVlEH+xaUPR7tvNdddPvmV5M5VLdr7cQuWE3hzA4jw==",
|
||||
"requires": {}
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
|
@ -24,6 +24,7 @@
|
||||
"docx": "^8.5.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-config-next": "13.4.4",
|
||||
"lucide-react": "^0.451.0",
|
||||
"next": "13.4.4",
|
||||
"open-resume": "file:",
|
||||
"pdfjs": "^2.5.0",
|
||||
|
BIN
public/fonts/Garamond-Bold.ttf
Normal file
BIN
public/fonts/Garamond-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Garamond-Regular.ttf
Normal file
BIN
public/fonts/Garamond-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Helvetica-Bold.ttf
Normal file
BIN
public/fonts/Helvetica-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Helvetica.ttf
Normal file
BIN
public/fonts/Helvetica.ttf
Normal file
Binary file not shown.
@ -10,7 +10,8 @@
|
||||
@font-face {font-family: "OpenSans"; src: url("/fonts/OpenSans-Bold.ttf"); font-weight: bold;}
|
||||
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Regular.ttf");}
|
||||
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Bold.ttf"); font-weight: bold;}
|
||||
|
||||
@font-face {font-family: "Helevetica"; src: url("/fonts/Helevetica.ttf");}
|
||||
@font-face {font-family: "Helevetica"; src: url("/fonts/Helevetica-Bold.ttf"); font-weight: bold;}
|
||||
/* Serif Fonts */
|
||||
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Regular.ttf");}
|
||||
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Bold.ttf"); font-weight: bold;}
|
||||
@ -22,3 +23,6 @@
|
||||
@font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Bold.ttf"); font-weight: bold;}
|
||||
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Regular.ttf");}
|
||||
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Bold.ttf"); font-weight: bold;}
|
||||
@font-face {font-family: "Garamond"; src: url("/fonts/Garamond-Regular.ttf");}
|
||||
@font-face {font-family: "Garamond"; src: url("/fonts/Garamond-Bold.ttf"); font-weight: bold;}
|
||||
|
||||
|
@ -6,7 +6,7 @@ export async function POST(request: NextRequest) {
|
||||
const { prompt }: { prompt: string } = await request.json();
|
||||
|
||||
const cohere = new CohereClientV2({
|
||||
token: 'exWH6pp1G5wT7tpZ6ryCoVgE0n9w7e6l2Wr4CMx0',
|
||||
token: '',
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -1,32 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "../contexts/ThemeContext"; // Import the useTheme hook
|
||||
|
||||
const DarkModeToggle = () => {
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// On initial load, set the theme based on user's preference
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme === "dark" || (!storedTheme && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
setIsDarkMode(true);
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
setIsDarkMode(false);
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem("theme", "light");
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
}
|
||||
setIsDarkMode(!isDarkMode);
|
||||
};
|
||||
const { isDarkMode, toggleTheme } = useTheme(); // Get dark mode state and toggle function from context
|
||||
|
||||
return (
|
||||
<button
|
||||
@ -35,11 +11,11 @@ const DarkModeToggle = () => {
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<>
|
||||
<span className="mr-2">🌞</span>
|
||||
<span className="mr-2">🌞</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="mr-2">🌙</span>
|
||||
<span className="mr-2">🌙</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
@ -28,7 +28,7 @@ export const InputGroupWrapper = ({
|
||||
children?: React.ReactNode;
|
||||
wordCount?: number;
|
||||
}) => (
|
||||
<label className={`text-base font-medium text-gray-700 ${className}`}>
|
||||
<label className={`text-base font-medium text-gray-700 ${className} dark:bg-white`}>
|
||||
{label}
|
||||
{wordCount !== undefined && (
|
||||
<span className="ml-2 text-sm text-gray-500">{wordCount} words</span>
|
||||
|
@ -11,4 +11,12 @@ export const THEME_COLORS = [
|
||||
"#0ea5e9", // Sky-500
|
||||
"#818cf8", // Indigo-400
|
||||
"#6366f1", // Indigo-500
|
||||
"#964B00", //brwon
|
||||
"#D3D3D3", //gray
|
||||
"#F3CFC6", //pink
|
||||
"#9370DB", //medium purple
|
||||
"#6A5ACD", //stale blue
|
||||
"#008080", //teal
|
||||
"#001F3F", //navy blue
|
||||
"#228B22", //forest green
|
||||
];
|
||||
|
@ -1,15 +1,8 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTheme } from "../contexts/ThemeContext"; // Import useTheme
|
||||
|
||||
/**
|
||||
* A simple Tooltip component that shows tooltip text center below children on hover and on focus
|
||||
*
|
||||
* @example
|
||||
* <Tooltip text="Tooltip Text">
|
||||
* <div>Hello</div>
|
||||
* </Tooltip>
|
||||
*/
|
||||
export const Tooltip = ({
|
||||
text,
|
||||
children,
|
||||
@ -19,14 +12,14 @@ export const Tooltip = ({
|
||||
}) => {
|
||||
const spanRef = useRef<HTMLSpanElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const { isDarkMode } = useTheme(); // Access the dark mode state
|
||||
|
||||
const [tooltipPos, setTooltipPos] = useState({ top: 0, left: 0 });
|
||||
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const showTooltip = () => setShow(true);
|
||||
const hideTooltip = () => setShow(false);
|
||||
|
||||
// Hook to set tooltip position to be right below children and centered
|
||||
useEffect(() => {
|
||||
const span = spanRef.current;
|
||||
const tooltip = tooltipRef.current;
|
||||
@ -57,7 +50,9 @@ export const Tooltip = ({
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
role="tooltip"
|
||||
className="absolute left-0 top-0 z-10 w-max rounded-md bg-gray-200 dark:bg-gray-600 px-2 py-0.5 text-sm text-black dark:text-white" // Added light and dark mode styles
|
||||
className={`absolute left-0 top-0 z-10 w-max rounded-md px-2 py-0.5 text-sm ${
|
||||
isDarkMode ? "bg-gray-600 text-white" : "bg-gray-200 text-black"
|
||||
}`} // Toggle dark mode classes based on state
|
||||
style={{
|
||||
left: `${tooltipPos.left}px`,
|
||||
top: `${tooltipPos.top}px`,
|
||||
|
@ -2,8 +2,9 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import logoSrc from "public/logo.svg";
|
||||
import logoSrc from "public/logo.svg"; // Ensure your logo is an SVG
|
||||
import { cx } from "lib/cx";
|
||||
import DarkModeToggle from "./DarkModeToggle";
|
||||
|
||||
export const TopNavBar = () => {
|
||||
const pathName = usePathname();
|
||||
@ -18,12 +19,15 @@ export const TopNavBar = () => {
|
||||
)}
|
||||
>
|
||||
<div className="flex h-10 w-full items-center justify-between">
|
||||
<Link href="/">
|
||||
<span className="sr-only">OpenResume</span>
|
||||
<Link href="/" className="flex items-center">
|
||||
{/* Ensure consistent styling */}
|
||||
<span className="text-black dark:text-white text-lg font-bold mr-2">
|
||||
OpenResume
|
||||
</span>
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt="OpenResume Logo"
|
||||
className="h-8 w-full"
|
||||
className="h-8"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
@ -37,12 +41,13 @@ export const TopNavBar = () => {
|
||||
].map(([href, text]) => (
|
||||
<Link
|
||||
key={text}
|
||||
className="rounded-md px-1.5 py-2 text-gray-500 hover:bg-gray-100 focus-visible:bg-gray-100 lg:px-4"
|
||||
className="rounded-md px-1.5 py-2 text-gray-500 hover:bg-gray-100 focus-visible:bg-gray-100 dark:text-gray-300 lg:px-4"
|
||||
href={href}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className="ml-1 mt-1">
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=xitanggg&repo=open-resume&type=star&count=true"
|
||||
@ -52,6 +57,7 @@ export const TopNavBar = () => {
|
||||
title="GitHub"
|
||||
/>
|
||||
</div>
|
||||
<DarkModeToggle />
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -17,13 +17,13 @@ export const Table = ({
|
||||
const tableBody = table.slice(1);
|
||||
return (
|
||||
<table
|
||||
className={cx("w-full divide-y border text-sm text-gray-900", className)}
|
||||
className={cx("w-full divide-y border text-sm text-gray-900 dark:text-gray-300", className)}
|
||||
>
|
||||
<thead className="divide-y bg-gray-50 text-left align-top">
|
||||
<thead className="divide-y bg-gray-50 text-left align-top ">
|
||||
{title && (
|
||||
<tr className="divide-x bg-gray-50">
|
||||
<tr className="divide-x bg-gray-50 ">
|
||||
<th
|
||||
className="px-2 py-1.5 font-bold"
|
||||
className="px-2 py-1.5 font-bold "
|
||||
scope="colSpan"
|
||||
colSpan={tableHeader.length}
|
||||
>
|
||||
@ -31,7 +31,7 @@ export const Table = ({
|
||||
</th>
|
||||
</tr>
|
||||
)}
|
||||
<tr className="divide-x bg-gray-50">
|
||||
<tr className="divide-x bg-primary">
|
||||
{tableHeader.map((item, idx) => (
|
||||
<th className="px-2 py-1.5 font-semibold" scope="col" key={idx}>
|
||||
{item}
|
||||
|
@ -33,6 +33,7 @@ const SANS_SERIF_ENGLISH_FONT_FAMILIES = [
|
||||
"Montserrat",
|
||||
"OpenSans",
|
||||
"Raleway",
|
||||
"Helevetica"
|
||||
] as const;
|
||||
|
||||
const SERIF_ENGLISH_FONT_FAMILIES = [
|
||||
@ -41,6 +42,7 @@ const SERIF_ENGLISH_FONT_FAMILIES = [
|
||||
"RobotoSlab",
|
||||
"PlayfairDisplay",
|
||||
"Merriweather",
|
||||
"Garamond"
|
||||
] as const;
|
||||
|
||||
export const ENGLISH_FONT_FAMILIES = [
|
||||
@ -67,12 +69,14 @@ export const FONT_FAMILY_TO_STANDARD_SIZE_IN_PT: Record<FontFamily, number> = {
|
||||
Montserrat: 10,
|
||||
OpenSans: 10,
|
||||
Raleway: 10,
|
||||
Helevetica:12,
|
||||
// Serif Fonts
|
||||
Caladea: 11,
|
||||
Lora: 11,
|
||||
RobotoSlab: 10,
|
||||
PlayfairDisplay: 10,
|
||||
Merriweather: 10,
|
||||
Garamond:12,
|
||||
// Non-English Fonts
|
||||
NotoSansSC: 11,
|
||||
};
|
||||
@ -84,12 +88,14 @@ export const FONT_FAMILY_TO_DISPLAY_NAME: Record<FontFamily, string> = {
|
||||
Montserrat: "Montserrat",
|
||||
OpenSans: "Open Sans",
|
||||
Raleway: "Raleway",
|
||||
Helevetica:"Helevetica",
|
||||
// Serif Fonts
|
||||
Caladea: "Caladea",
|
||||
Lora: "Lora",
|
||||
RobotoSlab: "Roboto Slab",
|
||||
PlayfairDisplay: "Playfair Display",
|
||||
Merriweather: "Merriweather",
|
||||
Garamond:"Garamond",
|
||||
// Non-English Fonts
|
||||
NotoSansSC: "思源黑体(简体)",
|
||||
};
|
||||
|
56
src/app/contexts/ThemeContext.tsx
Normal file
56
src/app/contexts/ThemeContext.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
import { createContext, useState, useEffect, ReactNode, useContext } from "react";
|
||||
|
||||
// Define the shape of the context
|
||||
interface ThemeContextType {
|
||||
isDarkMode: boolean;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
// Create the context with a default value
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
// Create a provider component
|
||||
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
if (storedTheme === "dark" || (!storedTheme && prefersDark)) {
|
||||
setIsDarkMode(true);
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
setIsDarkMode(false);
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem("theme", "light");
|
||||
setIsDarkMode(false);
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
setIsDarkMode(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom hook to use the ThemeContext in components
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
"use client"
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Send } from 'lucide-react';
|
||||
|
||||
interface Message {
|
||||
type: 'user' | 'cohere';
|
||||
@ -10,11 +10,27 @@ interface Message {
|
||||
export default function ChatPage() {
|
||||
const [userMessage, setUserMessage] = useState<string>('');
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(scrollToBottom, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||
}
|
||||
}, [userMessage]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
// Add user message to chat
|
||||
if (!userMessage.trim()) return;
|
||||
|
||||
const newMessages = [...messages, { type: 'user', text: userMessage }];
|
||||
setMessages(newMessages);
|
||||
setUserMessage('');
|
||||
@ -31,45 +47,64 @@ export default function ChatPage() {
|
||||
const data = await response.json();
|
||||
const cohereMessage = data.message;
|
||||
|
||||
// Add cohere response to chat
|
||||
setMessages([...newMessages, { type: 'cohere', text: cohereMessage }]);
|
||||
} catch (error) {
|
||||
console.error('Error sending request:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-screen bg-gray-50">
|
||||
{/* Chat display area */}
|
||||
<div className="flex-grow p-4 overflow-y-auto space-y-4">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-2 max-w-xs rounded-lg ${
|
||||
message.type === 'user' ? 'bg-blue-500 text-white self-end' : 'bg-gray-200 text-black self-start'
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex-grow p-4 overflow-y-auto">
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`p-3 rounded-lg max-w-md ${
|
||||
message.type === 'user'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white text-gray-800 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input section */}
|
||||
<form onSubmit={handleSubmit} className="p-4 border-t flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={userMessage}
|
||||
onChange={(e) => setUserMessage(e.target.value)}
|
||||
placeholder="Enter your message..."
|
||||
className="border p-2 flex-grow rounded-l-md"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 text-white p-2 rounded-r-md hover:bg-blue-600"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
<div className="border-t bg-white p-4">
|
||||
<form onSubmit={handleSubmit} className="max-w-3xl mx-auto flex items-end">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={userMessage}
|
||||
onChange={(e) => setUserMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type your message here..."
|
||||
className="flex-grow p-3 border rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none overflow-hidden"
|
||||
rows={1}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 text-white p-3 rounded-r-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 h-[42px]"
|
||||
>
|
||||
<Send size={20} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -44,7 +44,7 @@ export const Features = () => {
|
||||
{FEATURES.map(({ src, title, text }) => (
|
||||
<div className="px-2" key={title}>
|
||||
<div className="relative w-96 self-center pl-16">
|
||||
<dt className="text-2xl font-bold">
|
||||
<dt className="text-2xl font-bold dark:text-gray-300">
|
||||
<Image
|
||||
src={src}
|
||||
className="absolute left-0 top-1 h-12 w-12"
|
||||
@ -52,7 +52,7 @@ export const Features = () => {
|
||||
/>
|
||||
{title}
|
||||
</dt>
|
||||
<dd className="mt-2">{text}</dd>
|
||||
<dd className="mt-2 dark:text-gray-300">{text}</dd>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -12,8 +12,8 @@ export const Hero = () => {
|
||||
<br />
|
||||
resume easily
|
||||
</h1>
|
||||
<p className="mt-3 text-lg lg:mt-5 lg:text-xl">
|
||||
With this free, open-source, and powerful resume builder
|
||||
<p className="mt-3 text-lg lg:mt-5 lg:text-xl dark:text-gray-300">
|
||||
With this free, open-source, and powerful resume
|
||||
</p>
|
||||
<Link href="/resume-import" className="btn-primary mt-6 lg:mt-14">
|
||||
Create Resume <span aria-hidden="true">→</span>
|
||||
|
@ -125,8 +125,8 @@ const QAS = [
|
||||
export const QuestionsAndAnswers = () => {
|
||||
return (
|
||||
<section className="mx-auto max-w-3xl divide-y divide-gray-300 lg:mt-4 lg:px-2">
|
||||
<h2 className="text-center text-3xl font-bold">Questions & Answers</h2>
|
||||
<div className="mt-6 divide-y divide-gray-300">
|
||||
<h2 className="text-center text-3xl font-bold dark:text-gray-300">Questions & Answers</h2>
|
||||
<div className="mt-6 divide-y divide-gray-300 dark:text-gray-300">
|
||||
{QAS.map(({ question, answer }) => (
|
||||
<div key={question} className="py-6">
|
||||
<h3 className="font-semibold leading-7">{question}</h3>
|
||||
|
@ -64,7 +64,7 @@ export const Testimonials = ({ children }: { children?: React.ReactNode }) => {
|
||||
|
||||
return (
|
||||
<section className="mx-auto -mt-2 px-8 pb-24">
|
||||
<h2 className="mb-8 text-center text-3xl font-bold">
|
||||
<h2 className="mb-8 text-center text-3xl font-bold dark:text-gray-300">
|
||||
People{" "}
|
||||
<Image src={heartSrc} alt="love" className="-mt-1 inline-block w-7" />{" "}
|
||||
OpenResume
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "globals.css";
|
||||
import { TopNavBar } from "components/TopNavBar";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import DarkModeToggle from "components/DarkModeToggle";
|
||||
import { ThemeProvider } from "contexts/ThemeContext"; // Import ThemeProvider
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@ -11,12 +11,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="bg-white dark:bg-gray-900 text-black dark:text-white">
|
||||
<TopNavBar />
|
||||
<div className="flex top-4 right-4 z-50">
|
||||
<DarkModeToggle /> {/* Positioning the button at the center */}
|
||||
</div>
|
||||
{children}
|
||||
<Analytics />
|
||||
<ThemeProvider>
|
||||
<TopNavBar />
|
||||
{children}
|
||||
<Analytics />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -3,59 +3,138 @@ import { Provider } from "react-redux";
|
||||
import { store } from "lib/redux/store";
|
||||
import { ResumeForm } from "components/ResumeForm";
|
||||
import { Resume } from "components/Resume";
|
||||
import ChatPage from "home/ChatPage";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { FaWandMagicSparkles } from "react-icons/fa6";
|
||||
import { Send, X } from 'lucide-react';
|
||||
|
||||
// Floating Chat Button Component
|
||||
interface FloatingChatButtonProps {
|
||||
toggleChat: () => void;
|
||||
interface Message {
|
||||
type: 'user' | 'cohere';
|
||||
text: string;
|
||||
}
|
||||
|
||||
// Floating Chat Button Component
|
||||
interface FloatingChatButtonProps {
|
||||
toggleChat: () => void;
|
||||
// Chat Sidebar Component
|
||||
interface ChatSidebarProps {
|
||||
isOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const FloatingChatButton: React.FC<FloatingChatButtonProps> = ({ toggleChat }) => (
|
||||
<button
|
||||
className="fixed bottom-20 right-5 z-50 flex items-center rounded-full bg-blue-500 p-3 text-white shadow-lg hover:bg-blue-600 focus:outline-none"
|
||||
onClick={toggleChat}
|
||||
>
|
||||
<FaWandMagicSparkles className="mr-2" /> {/* Margin to the right of the icon */}
|
||||
<span>Chat</span> {/* Use a span for proper text alignment */}
|
||||
</button>
|
||||
);
|
||||
const ChatSidebar: React.FC<ChatSidebarProps> = ({ isOpen, toggleSidebar }) => {
|
||||
const [userMessage, setUserMessage] = useState<string>('');
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Chat Window Component
|
||||
interface ChatWindowProps {
|
||||
toggleChat: () => void;
|
||||
}
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const ChatWindow: React.FC<ChatWindowProps> = ({ toggleChat }) => (
|
||||
<div className="fixed bottom-16 right-5 z-50 w-80 h-96 bg-white shadow-lg rounded-lg overflow-hidden">
|
||||
<div className="flex justify-between items-center p-2 bg-blue-500 text-white">
|
||||
<h3>Chat</h3>
|
||||
<button onClick={toggleChat} className="text-white">
|
||||
✖️
|
||||
</button>
|
||||
useEffect(scrollToBottom, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||
}
|
||||
}, [userMessage]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!userMessage.trim()) return;
|
||||
|
||||
const newMessages = [...messages, { type: 'user', text: userMessage }];
|
||||
setMessages(newMessages);
|
||||
setUserMessage('');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ prompt: userMessage }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const cohereMessage = data.message;
|
||||
|
||||
setMessages([...newMessages, { type: 'cohere', text: cohereMessage }]);
|
||||
} catch (error) {
|
||||
console.error('Error sending request:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`fixed top-0 right-0 h-full w-96 bg-white shadow-lg transform transition-transform duration-300 ease-in-out dark:bg-gray-800 ${isOpen ? 'translate-x-0' : 'translate-x-full'}`}>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center p-4 border-b dark:border-gray-700">
|
||||
<h2 className="text-xl font-semibold dark:text-white">Chat</h2>
|
||||
<button onClick={toggleSidebar} className="text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200">
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto p-4">
|
||||
<div className="space-y-4">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`p-3 rounded-lg max-w-[80%] ${
|
||||
message.type === 'user'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t p-4 dark:border-gray-700">
|
||||
<form onSubmit={handleSubmit} className="flex items-end">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={userMessage}
|
||||
onChange={(e) => setUserMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type your message here..."
|
||||
className="flex-grow p-2 border rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none overflow-hidden dark:bg-gray-600 dark:text-gray-200"
|
||||
rows={1}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 text-white p-2 rounded-r-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 h-[38px]"
|
||||
>
|
||||
<Send size={20} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8 h-full">
|
||||
<ChatPage />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const Create: React.FC = () => {
|
||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const toggleChat = () => {
|
||||
setIsChatOpen(!isChatOpen);
|
||||
const toggleSidebar = () => {
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<main className="relative h-full w-full overflow-hidden bg-gray-50">
|
||||
<main className="relative h-full w-full overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div className="grid grid-cols-3 md:grid-cols-6">
|
||||
<div className="col-span-3">
|
||||
<ResumeForm />
|
||||
@ -65,11 +144,17 @@ const Create: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Chat Button */}
|
||||
<FloatingChatButton toggleChat={toggleChat} />
|
||||
{!isSidebarOpen && ( // Hide button when sidebar is open
|
||||
<button
|
||||
className="fixed bottom-10 right-5 z-50 flex items-center rounded-full bg-blue-500 p-3 text-white shadow-lg hover:bg-blue-600 focus:outline-none"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<FaWandMagicSparkles className="mr-2" />
|
||||
<span>Chat</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Conditionally render Chat Window with ChatPage */}
|
||||
{isChatOpen && <ChatWindow toggleChat={toggleChat} />}
|
||||
<ChatSidebar isOpen={isSidebarOpen} toggleSidebar={toggleSidebar} />
|
||||
</main>
|
||||
</Provider>
|
||||
);
|
||||
|
@ -103,7 +103,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
|
||||
return (
|
||||
<article className="mt-10">
|
||||
<Heading className="text-primary !mt-0 border-t-2 pt-8">
|
||||
<Heading className="text-primary !mt-0 border-t-2 pt-8 ">
|
||||
Resume Parser Algorithm Deep Dive
|
||||
</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
@ -113,7 +113,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
language)
|
||||
</Paragraph>
|
||||
{/* Step 1. Read the text items from a PDF file */}
|
||||
<Heading level={2}>Step 1. Read the text items from a PDF file</Heading>
|
||||
<Heading className="dark:text-gray-300" level={2}>Step 1. Read the text items from a PDF file</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
A PDF file is a standardized file format defined by the{" "}
|
||||
<Link href="https://www.iso.org/standard/51502.html">
|
||||
@ -140,7 +140,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
(Note that x,y position is relative to the bottom left corner of the
|
||||
page, which is the origin 0,0)
|
||||
</Paragraph>
|
||||
<div className="mt-4 max-h-72 overflow-y-scroll border scrollbar scrollbar-track-gray-100 scrollbar-thumb-gray-200 scrollbar-w-3">
|
||||
<div className="mt-4 max-h-72 overflow-y-scroll border scrollbar scrollbar-track-gray-100 scrollbar-thumb-gray-200 scrollbar-w-3 dark:text-gray-300">
|
||||
<Table
|
||||
table={step1TextItemsTable}
|
||||
className="!border-none"
|
||||
@ -148,7 +148,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
/>
|
||||
</div>
|
||||
{/* Step 2. Group text items into lines */}
|
||||
<Heading level={2}>Step 2. Group text items into lines</Heading>
|
||||
<Heading className="dark:text-gray-300" level={2}>Step 2. Group text items into lines</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
The extracted text items aren't ready to use yet and have 2 main issues:
|
||||
</Paragraph>
|
||||
@ -215,7 +215,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
<Table table={step2LinesTable} className="!border-none" />
|
||||
</div>
|
||||
{/* Step 3. Group lines into sections */}
|
||||
<Heading level={2}>Step 3. Group lines into sections</Heading>
|
||||
<Heading className="dark:text-gray-300" level={2}>Step 3. Group lines into sections</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
At step 2, the resume parser starts building contexts and associations
|
||||
to text items by first grouping them into lines. Step 3 continues the
|
||||
@ -262,13 +262,13 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
</Paragraph>
|
||||
<Step3SectionsTable sections={sections} />
|
||||
{/* Step 4. Extract resume from sections */}
|
||||
<Heading level={2}>Step 4. Extract resume from sections</Heading>
|
||||
<Heading className="dark:text-gray-300" level={2}>Step 4. Extract resume from sections</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
Step 4 is the last step of the resume parsing process and is also the
|
||||
core of the resume parser, where it extracts resume information from the
|
||||
sections.
|
||||
</Paragraph>
|
||||
<Heading level={3}>Feature Scoring System</Heading>
|
||||
<Heading className="dark:text-gray-300" level={3}>Feature Scoring System</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
The gist of the extraction engine is a feature scoring system. Each
|
||||
resume attribute to be extracted has a custom feature sets, where each
|
||||
@ -297,7 +297,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
indicating they are very unlikely to be the targeted attribute)
|
||||
</Paragraph>
|
||||
)}
|
||||
<Heading level={3}>Feature Sets</Heading>
|
||||
<Heading className="dark:text-gray-300" level={3}>Feature Sets</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
Having explained the feature scoring system, we can dive more into how
|
||||
feature sets are constructed for a resume attribute. It follows 2
|
||||
@ -318,7 +318,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
title="Name Feature Sets"
|
||||
className="mt-4"
|
||||
/>
|
||||
<Heading level={3}>Core Feature Function</Heading>
|
||||
<Heading className="dark:text-gray-300" level={3}>Core Feature Function</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
Each resume attribute has multiple feature sets. They can be found in
|
||||
the source code under the extract-resume-from-sections folder and we
|
||||
@ -327,7 +327,7 @@ export const ResumeParserAlgorithmArticle = ({
|
||||
core feature function below.
|
||||
</Paragraph>
|
||||
<Table table={step4CoreFeatureFunctionTable} className="mt-4" />
|
||||
<Heading level={3}>Special Case: Subsections</Heading>
|
||||
<Heading className="dark:text-gray-300" level={3}>Special Case: Subsections</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
The last thing that is worth mentioning is subsections. For profile
|
||||
section, we can directly pass all the text items to the feature scoring
|
||||
@ -461,7 +461,7 @@ const Step3SectionsTable = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 max-h-96 overflow-y-scroll border scrollbar scrollbar-track-gray-100 scrollbar-thumb-gray-200 scrollbar-w-3">
|
||||
<div className="mt-4 max-h-96 overflow-y-scroll border scrollbar scrollbar-track-gray-100 scrollbar-thumb-gray-200 scrollbar-w-3 dark:text-gray-300">
|
||||
<Table
|
||||
table={table}
|
||||
className="!border-none"
|
||||
|
@ -5,7 +5,7 @@ import { deepClone } from "lib/deep-clone";
|
||||
import { cx } from "lib/cx";
|
||||
|
||||
const TableRowHeader = ({ children }: { children: React.ReactNode }) => (
|
||||
<tr className="divide-x bg-gray-50">
|
||||
<tr className="divide-x bg-primary"> {/* Updated to bg-primary */}
|
||||
<th className="px-3 py-2 font-semibold" scope="colgroup" colSpan={2}>
|
||||
{children}
|
||||
</th>
|
||||
@ -57,8 +57,8 @@ export const ResumeTable = ({ resume }: { resume: Resume }) => {
|
||||
skills.unshift(featuredSkills);
|
||||
}
|
||||
return (
|
||||
<table className="mt-2 w-full border text-sm text-gray-900">
|
||||
<tbody className="divide-y text-left align-top">
|
||||
<table className="mt-2 w-full border text-sm text-gray-900 dark:text-gray-300 ">
|
||||
<tbody className="divide-y text-left align-top ">
|
||||
<TableRowHeader>Profile</TableRowHeader>
|
||||
<TableRow label="Name" value={resume.profile.name} />
|
||||
<TableRow label="Email" value={resume.profile.email} />
|
||||
@ -119,12 +119,12 @@ export const ResumeTable = ({ resume }: { resume: Resume }) => {
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
{resume.awards.length > 0 && (
|
||||
{resume.awards.length > 0 && (
|
||||
<TableRowHeader>Awards</TableRowHeader>
|
||||
)}
|
||||
{resume.awards.map((award, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<TableRow label="award" value={award.award} />
|
||||
<TableRow label="Award" value={award.award} />
|
||||
<TableRow label="Date" value={award.date} />
|
||||
<TableRow
|
||||
label="Descriptions"
|
||||
|
@ -62,13 +62,13 @@ export default function ResumeParser() {
|
||||
</section>
|
||||
<FlexboxSpacer maxWidth={45} className="hidden md:block" />
|
||||
</div>
|
||||
<div className="flex px-6 text-gray-900 md:col-span-3 md:h-[calc(100vh-var(--top-nav-bar-height))] md:overflow-y-scroll">
|
||||
<div className="flex px-6 text-gray-900 md:col-span-3 md:h-[calc(100vh-var(--top-nav-bar-height))] md:overflow-y-scroll dark:text-gray-300 ">
|
||||
<FlexboxSpacer maxWidth={45} className="hidden md:block" />
|
||||
<section className="max-w-[600px] grow">
|
||||
<Heading className="text-primary !mt-4">
|
||||
Resume Parser Playground
|
||||
</Heading>
|
||||
<Paragraph smallMarginTop={true}>
|
||||
<Paragraph smallMarginTop={true} className="dark:text-gray-300">
|
||||
This playground showcases the OpenResume resume parser and its
|
||||
ability to parse information from a resume PDF. Click around the
|
||||
PDF examples below to observe different parsing results.
|
||||
@ -118,7 +118,7 @@ export default function ResumeParser() {
|
||||
Resume Parsing Results
|
||||
</Heading>
|
||||
<ResumeTable resume={resume} />
|
||||
<ResumeParserAlgorithmArticle
|
||||
<ResumeParserAlgorithmArticle
|
||||
textItems={textItems}
|
||||
lines={lines}
|
||||
sections={sections}
|
||||
|
Loading…
Reference in New Issue
Block a user