Skip to content

Commit cff5aa3

Browse files
Spectualclaude
andcommitted
合并中低优先级优化 + 功能增强:8项改进
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents 63130aa + 2f50663 commit cff5aa3

File tree

8 files changed

+100
-57
lines changed

8 files changed

+100
-57
lines changed

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32" />
6+
<link rel="preconnect" href="https://fonts.googleapis.com" />
7+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
8+
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" />
69
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
710
<meta name="description" content="Yifei Bao — AI/ML Engineer at Boston University. Chat with an AI assistant to learn about my background, projects, and experience." />
811
<meta name="author" content="Yifei Bao" />

src/App.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { Component, type ReactNode } from "react";
1+
import { Component, lazy, Suspense, type ReactNode } from "react";
22
import { Toaster } from "@/components/ui/toaster";
33
import { Toaster as Sonner } from "@/components/ui/sonner";
44
import { TooltipProvider } from "@/components/ui/tooltip";
55
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
66
import { HashRouter, Routes, Route } from "react-router-dom";
77
import Index from "./pages/Index";
8-
import Resume from "./pages/Resume";
9-
import Projects from "./pages/Projects";
108
import NotFound from "./pages/NotFound";
119

10+
const Resume = lazy(() => import("./pages/Resume"));
11+
const Projects = lazy(() => import("./pages/Projects"));
12+
1213
class ErrorBoundary extends Component<
1314
{ children: ReactNode },
1415
{ hasError: boolean; error: Error | null }
@@ -59,12 +60,14 @@ const App = () => (
5960
<Toaster />
6061
<Sonner />
6162
<HashRouter>
62-
<Routes>
63-
<Route path="/" element={<Index />} />
64-
<Route path="/resume" element={<Resume />} />
65-
<Route path="/projects" element={<Projects />} />
66-
<Route path="*" element={<NotFound />} />
67-
</Routes>
63+
<Suspense fallback={null}>
64+
<Routes>
65+
<Route path="/" element={<Index />} />
66+
<Route path="/resume" element={<Resume />} />
67+
<Route path="/projects" element={<Projects />} />
68+
<Route path="*" element={<NotFound />} />
69+
</Routes>
70+
</Suspense>
6871
</HashRouter>
6972
</TooltipProvider>
7073
</QueryClientProvider>

src/components/BootSequence.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const BootSequence = ({ onComplete }: BootSequenceProps) => {
2525
completedRef.current = true;
2626
timersRef.current.forEach(clearTimeout);
2727
setFading(true);
28-
setTimeout(onComplete, 450);
28+
setTimeout(onComplete, 380);
2929
}, [onComplete]);
3030

3131
useEffect(() => {

src/components/ChatSection.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const ChatSection = () => {
7070
const isInitialLoad = useRef(true);
7171
const scrollContainerRef = useRef<HTMLDivElement>(null);
7272
const inputRef = useRef<HTMLInputElement>(null);
73+
const historyRef = useRef<string[]>([]);
74+
const historyIndexRef = useRef(-1);
75+
const pendingInputRef = useRef("");
7376

7477
const isNearBottom = (): boolean => {
7578
const el = scrollContainerRef.current;
@@ -113,8 +116,12 @@ const ChatSection = () => {
113116
// Finish any ongoing typewriter immediately
114117
setTypingMessageId(null);
115118

119+
historyRef.current = [messageText, ...historyRef.current];
120+
historyIndexRef.current = -1;
121+
pendingInputRef.current = "";
122+
116123
const userMessage: Message = {
117-
id: Date.now().toString(),
124+
id: crypto.randomUUID(),
118125
text: messageText,
119126
isUser: true,
120127
timestamp: new Date(),
@@ -127,7 +134,7 @@ const ChatSection = () => {
127134

128135
try {
129136
const response = await sendMessage(messageText);
130-
const aiId = (Date.now() + 1).toString();
137+
const aiId = crypto.randomUUID();
131138
const aiMessage: Message = {
132139
id: aiId,
133140
text: response.success
@@ -145,7 +152,7 @@ const ChatSection = () => {
145152
setMessages((prev) => [
146153
...prev,
147154
{
148-
id: (Date.now() + 1).toString(),
155+
id: crypto.randomUUID(),
149156
text: "Sorry, I'm having trouble connecting right now. Please try again later.",
150157
isUser: false,
151158
timestamp: new Date(),
@@ -161,6 +168,26 @@ const ChatSection = () => {
161168
if (e.key === "Enter" && !e.shiftKey) {
162169
e.preventDefault();
163170
handleSendMessage(inputValue);
171+
return;
172+
}
173+
const history = historyRef.current;
174+
if (e.key === "ArrowUp" && history.length > 0) {
175+
e.preventDefault();
176+
if (historyIndexRef.current === -1) {
177+
pendingInputRef.current = inputValue;
178+
}
179+
const next = Math.min(historyIndexRef.current + 1, history.length - 1);
180+
historyIndexRef.current = next;
181+
setInputValue(history[next]);
182+
} else if (e.key === "ArrowDown") {
183+
e.preventDefault();
184+
if (historyIndexRef.current <= 0) {
185+
historyIndexRef.current = -1;
186+
setInputValue(pendingInputRef.current);
187+
} else {
188+
historyIndexRef.current--;
189+
setInputValue(history[historyIndexRef.current]);
190+
}
164191
}
165192
};
166193

src/components/ProfileSection.tsx

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,28 @@ const SKILL_LEVELS = [
1313

1414
const BAR_TOTAL = 20;
1515

16-
const SkillBar = ({ name, level, delay }: { name: string; level: number; delay: number }) => {
16+
const SkillBar = ({ name, level, delay, started }: { name: string; level: number; delay: number; started: boolean }) => {
1717
const [filled, setFilled] = useState(0);
18-
const ref = useRef<HTMLDivElement>(null);
1918
const target = Math.round((level / 100) * BAR_TOTAL);
2019

2120
useEffect(() => {
22-
const el = ref.current;
23-
if (!el) return;
24-
25-
const observer = new IntersectionObserver(
26-
([entry]) => {
27-
if (!entry.isIntersecting) return;
28-
observer.disconnect();
29-
let current = 0;
30-
const step = () => {
31-
if (current >= target) return;
32-
current++;
33-
setFilled(current);
34-
setTimeout(step, 45);
35-
};
36-
setTimeout(step, delay);
37-
},
38-
{ threshold: 0.5 }
39-
);
40-
observer.observe(el);
41-
return () => observer.disconnect();
42-
}, [target, delay]);
21+
if (!started) return;
22+
let current = 0;
23+
let timer: ReturnType<typeof setTimeout>;
24+
const step = () => {
25+
if (current >= target) return;
26+
current++;
27+
setFilled(current);
28+
timer = setTimeout(step, 45);
29+
};
30+
timer = setTimeout(step, delay);
31+
return () => clearTimeout(timer);
32+
}, [started, target, delay]);
4333

4434
const empty = BAR_TOTAL - filled;
4535

4636
return (
4737
<div
48-
ref={ref}
4938
style={{
5039
display: "flex",
5140
alignItems: "center",
@@ -158,6 +147,25 @@ const AutoTypeCLI = () => {
158147
const ProfileSection = () => {
159148
const skills = personalInfo.skills;
160149

150+
// Single IntersectionObserver for all skill bars
151+
const [skillsStarted, setSkillsStarted] = useState(false);
152+
const skillsContainerRef = useRef<HTMLDivElement>(null);
153+
154+
useEffect(() => {
155+
const el = skillsContainerRef.current;
156+
if (!el) return;
157+
const observer = new IntersectionObserver(
158+
([entry]) => {
159+
if (!entry.isIntersecting) return;
160+
observer.disconnect();
161+
setSkillsStarted(true);
162+
},
163+
{ threshold: 0.3 }
164+
);
165+
observer.observe(el);
166+
return () => observer.disconnect();
167+
}, []);
168+
161169
// Chained typewriter: name first, then role after name finishes
162170
const [nameTyped, setNameTyped] = useState(false);
163171
const { displayed: displayedName } = useTypewriter(personalInfo.name, {
@@ -391,12 +399,12 @@ const ProfileSection = () => {
391399
</div>
392400

393401
{/* Skills progress bars */}
394-
<div style={{ marginBottom: "12px" }}>
402+
<div ref={skillsContainerRef} style={{ marginBottom: "12px" }}>
395403
<div style={{ fontSize: "11px", color: "var(--term-dim)", marginBottom: "8px" }}>
396404
# proficiency
397405
</div>
398406
{SKILL_LEVELS.map((s, i) => (
399-
<SkillBar key={s.name} name={s.name} level={s.level} delay={i * 120} />
407+
<SkillBar key={s.name} name={s.name} level={s.level} delay={i * 120} started={skillsStarted} />
400408
))}
401409
</div>
402410

src/hooks/useTypewriter.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,26 @@ export const useTypewriter = (text: string, options?: TypewriterOptions) => {
2424
setDisplayed("");
2525
setDone(false);
2626
let i = 0;
27+
let cancelled = false;
2728
let delayTimer: ReturnType<typeof setTimeout>;
28-
let intervalTimer: ReturnType<typeof setInterval>;
29-
30-
delayTimer = setTimeout(() => {
31-
intervalTimer = setInterval(() => {
32-
i++;
33-
setDisplayed(text.slice(0, i));
34-
if (i >= text.length) {
35-
clearInterval(intervalTimer);
36-
setDone(true);
37-
onCompleteRef.current?.();
38-
}
39-
}, speed);
40-
}, startDelay);
29+
30+
const step = () => {
31+
if (cancelled) return;
32+
i++;
33+
setDisplayed(text.slice(0, i));
34+
if (i >= text.length) {
35+
setDone(true);
36+
onCompleteRef.current?.();
37+
} else {
38+
delayTimer = setTimeout(step, speed);
39+
}
40+
};
41+
42+
delayTimer = setTimeout(step, startDelay);
4143

4244
return () => {
45+
cancelled = true;
4346
clearTimeout(delayTimer);
44-
clearInterval(intervalTimer);
4547
};
4648
}, [text, speed, startDelay]);
4749

src/pages/Index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const Index = () => {
4141
{/* Main content */}
4242
<div
4343
className="page-enter"
44-
style={{ position: "relative", zIndex: 1, opacity: booting ? 0 : 1, transition: "opacity 0.3s" }}
44+
style={{ position: "relative", zIndex: 1, opacity: booting ? 0 : 1, transition: booting ? "none" : "opacity 0.45s ease-in" }}
4545
>
4646
<TerminalHeader />
4747

src/pages/Projects.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const Projects = () => {
5858

5959
return (
6060
<div
61-
key={index}
61+
key={project.name}
6262
className="project-card"
6363
style={{
6464
border: "1px solid var(--term-border)",
@@ -152,9 +152,9 @@ const Projects = () => {
152152

153153
{/* Tech tags */}
154154
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px" }}>
155-
{project.technologies.map((tech, techIndex) => (
155+
{project.technologies.map((tech) => (
156156
<span
157-
key={techIndex}
157+
key={tech}
158158
style={{
159159
border: "1px solid var(--term-border)",
160160
color: "var(--term-dim)",

0 commit comments

Comments
 (0)