Skip to content

Commit 0eb0872

Browse files
committed
Add getintouch section and update chat UI
1 parent c756488 commit 0eb0872

File tree

10 files changed

+213
-23
lines changed

10 files changed

+213
-23
lines changed

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"@fontsource/inter": "^5.2.6",
1314
"@radix-ui/react-avatar": "^1.0.4",
1415
"@radix-ui/react-dialog": "^1.0.5",
1516
"@radix-ui/react-dropdown-menu": "^2.0.6",

src/components/ChatSection.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,30 @@ const ChatSection = () => {
2727
const [isServerOnline, setIsServerOnline] = useState(true);
2828
const messagesEndRef = useRef<HTMLDivElement>(null);
2929
const isInitialLoad = useRef(true);
30+
const scrollContainerRef = useRef<HTMLDivElement>(null);
3031

31-
const scrollToBottom = () => {
32-
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
32+
const isNearBottom = (): boolean => {
33+
const el = scrollContainerRef.current;
34+
if (!el) return true;
35+
const threshold = 80; // px
36+
return el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
37+
};
38+
39+
const scrollChatToBottom = () => {
40+
const el = scrollContainerRef.current;
41+
if (!el) return;
42+
el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
3343
};
3444

3545
useEffect(() => {
3646
if (isInitialLoad.current) {
3747
isInitialLoad.current = false;
3848
return;
3949
}
40-
scrollToBottom();
50+
// Only auto-scroll if user is near bottom (reading latest) to avoid jumping to Get In Touch
51+
if (isNearBottom()) {
52+
scrollChatToBottom();
53+
}
4154
}, [messages]);
4255

4356
const predefinedQuestions = [
@@ -82,6 +95,8 @@ const ChatSection = () => {
8295
setMessages((prev) => [...prev, userMessage]);
8396
setInputValue("");
8497
setIsLoading(true);
98+
// When the sender is the user, always scroll the chat container to bottom
99+
requestAnimationFrame(scrollChatToBottom);
85100

86101
try {
87102
const response = await sendMessage(messageText);
@@ -163,7 +178,7 @@ const ChatSection = () => {
163178
</div>
164179

165180
{/* Chat Messages */}
166-
<div className="h-96 overflow-y-auto p-6 space-y-4 scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
181+
<div ref={scrollContainerRef} className="h-96 overflow-y-auto p-6 space-y-4 scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
167182
{messages.map((message) => (
168183
<MessageBubble key={message.id} message={message} />
169184
))}
@@ -182,20 +197,20 @@ const ChatSection = () => {
182197
</div>
183198

184199
{/* Input Area */}
185-
<div className="p-6 bg-gradient-to-r from-slate-800/20 to-purple-800/20 border-t border-white/10">
200+
<div className="p-6 bg-slate-900/30 border-t border-white/10">
186201
<div className="flex gap-3 mb-4">
187202
<Input
188203
value={inputValue}
189204
onChange={(e) => setInputValue(e.target.value)}
190205
onKeyPress={handleKeyPress}
191206
placeholder="Ask me anything about my background..."
192-
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400 backdrop-blur-sm rounded-xl"
207+
className="bg-slate-800/50 border-white/10 text-white placeholder:text-slate-400 backdrop-blur-sm rounded-xl"
193208
disabled={isLoading || !isServerOnline}
194209
/>
195210
<Button
196211
onClick={() => handleSendMessage(inputValue)}
197212
disabled={isLoading || !inputValue.trim() || !isServerOnline}
198-
className="bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600 rounded-xl px-6"
213+
className="rounded-xl px-6 bg-cyan-500/20 hover:bg-cyan-500/30 text-cyan-300 border border-cyan-500/30"
199214
>
200215
<ArrowRight size={18} />
201216
</Button>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { Mail, Phone, MapPin } from "lucide-react";
2+
import { useState } from "react";
3+
import { Input } from "@/components/ui/input";
4+
import { Textarea } from "@/components/ui/textarea";
5+
import { Button } from "@/components/ui/button";
6+
import { toast } from "@/components/ui/use-toast";
7+
8+
const GetInTouchSection = () => {
9+
const [form, setForm] = useState({ name: "", company: "", email: "", message: "" });
10+
const [submitting, setSubmitting] = useState(false);
11+
12+
const validate = () => {
13+
if (!form.name.trim()) {
14+
toast({ title: "Name is required" });
15+
return false;
16+
}
17+
if (!form.email.trim()) {
18+
toast({ title: "Email is required" });
19+
return false;
20+
}
21+
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email);
22+
if (!emailOk) {
23+
toast({ title: "Please enter a valid email" });
24+
return false;
25+
}
26+
if (!form.message.trim()) {
27+
toast({ title: "Message is required" });
28+
return false;
29+
}
30+
return true;
31+
};
32+
33+
const handleSubmit = (e: React.FormEvent) => {
34+
e.preventDefault();
35+
if (!validate()) return;
36+
setSubmitting(true);
37+
38+
const subject = encodeURIComponent(`Website Contact from ${form.name}${form.company ? ' - ' + form.company : ''}`);
39+
const bodyLines = [
40+
`Name: ${form.name}`,
41+
`Company: ${form.company || 'N/A'}`,
42+
`Email: ${form.email}`,
43+
"",
44+
"Message:",
45+
form.message,
46+
];
47+
const body = encodeURIComponent(bodyLines.join("\n"));
48+
const mailto = `mailto:baoyifei@bu.edu?subject=${subject}&body=${body}`;
49+
50+
window.location.href = mailto;
51+
setSubmitting(false);
52+
};
53+
54+
return (
55+
<section className="px-6 pt-2 pb-12">
56+
<div className="max-w-6xl mx-auto">
57+
<div className="backdrop-blur-xl bg-white/10 rounded-3xl border border-white/20 p-8 shadow-2xl">
58+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
59+
<div>
60+
<h2 className="text-3xl font-bold bg-gradient-to-r from-white to-cyan-400 bg-clip-text text-transparent mb-6">
61+
Get in Touch
62+
</h2>
63+
<form onSubmit={handleSubmit} className="space-y-6">
64+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
65+
<div>
66+
<label className="block text-slate-300 text-sm mb-2">Name *</label>
67+
<Input
68+
placeholder="Your name"
69+
value={form.name}
70+
onChange={(e) => setForm({ ...form, name: e.target.value })}
71+
/>
72+
</div>
73+
<div>
74+
<label className="block text-slate-300 text-sm mb-2">Company</label>
75+
<Input
76+
placeholder="Company (optional)"
77+
value={form.company}
78+
onChange={(e) => setForm({ ...form, company: e.target.value })}
79+
/>
80+
</div>
81+
</div>
82+
<div>
83+
<label className="block text-slate-300 text-sm mb-2">Email *</label>
84+
<Input
85+
type="email"
86+
placeholder="you@example.com"
87+
value={form.email}
88+
onChange={(e) => setForm({ ...form, email: e.target.value })}
89+
/>
90+
</div>
91+
<div>
92+
<label className="block text-slate-300 text-sm mb-2">Message *</label>
93+
<Textarea
94+
placeholder="Tell me about your needs and how I can help..."
95+
rows={6}
96+
value={form.message}
97+
onChange={(e) => setForm({ ...form, message: e.target.value })}
98+
/>
99+
</div>
100+
<div>
101+
<Button type="submit" disabled={submitting} className="bg-cyan-600/20 hover:bg-cyan-600/30 text-cyan-300 border border-cyan-500/30">
102+
{submitting ? "Opening email..." : "Send Message"}
103+
</Button>
104+
</div>
105+
</form>
106+
</div>
107+
108+
<div className="space-y-6">
109+
<h3 className="text-2xl font-semibold text-white">Let's Start a Conversation</h3>
110+
<p className="text-slate-300">
111+
I'm happy to connect about opportunities, collaboration, or any interesting ideas.
112+
</p>
113+
<div className="space-y-4">
114+
<div className="flex items-center gap-4 p-4 rounded-xl bg-white/5 border border-white/10">
115+
<div className="p-3 rounded-lg bg-cyan-600/20">
116+
<Mail className="text-cyan-400" />
117+
</div>
118+
<div>
119+
<div className="text-slate-400 text-sm">Email</div>
120+
<a href="mailto:baoyifei@bu.edu" className="text-white">baoyifei@bu.edu</a>
121+
</div>
122+
</div>
123+
<div className="flex items-center gap-4 p-4 rounded-xl bg-white/5 border border-white/10">
124+
<div className="p-3 rounded-lg bg-emerald-600/20">
125+
<Phone className="text-emerald-400" />
126+
</div>
127+
<div>
128+
<div className="text-slate-400 text-sm">Phone</div>
129+
<a href="tel:+18573403064" className="text-white">+1 8573403064</a>
130+
</div>
131+
</div>
132+
<div className="flex items-center gap-4 p-4 rounded-xl bg-white/5 border border-white/10">
133+
<div className="p-3 rounded-lg bg-blue-600/20">
134+
<MapPin className="text-blue-400" />
135+
</div>
136+
<div>
137+
<div className="text-slate-400 text-sm">Office</div>
138+
<div className="text-white">Boston</div>
139+
</div>
140+
</div>
141+
</div>
142+
</div>
143+
</div>
144+
</div>
145+
</div>
146+
</section>
147+
);
148+
};
149+
150+
export default GetInTouchSection;

src/components/MessageBubble.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ const MessageBubble = ({ message }: MessageBubbleProps) => {
1717
<div
1818
className={`max-w-xs lg:max-w-md px-4 py-3 rounded-2xl backdrop-blur-sm ${
1919
message.isUser
20-
? "bg-gradient-to-r from-cyan-500 to-purple-500 text-white shadow-lg"
20+
? "bg-cyan-500/20 text-cyan-100 border border-cyan-500/30 shadow-lg"
2121
: "bg-white/10 text-slate-100 border border-white/20"
2222
}`}
2323
>
2424
<div className="prose prose-sm prose-invert max-w-none">
2525
<ReactMarkdown>{message.text}</ReactMarkdown>
2626
</div>
27-
<p className={`text-xs mt-2 ${message.isUser ? "text-cyan-100" : "text-slate-400"}`}>
27+
<p className={`text-xs mt-2 ${message.isUser ? "text-cyan-200/80" : "text-slate-400"}`}>
2828
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
2929
</p>
3030
</div>

src/components/ui/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
88
<input
99
type={type}
1010
className={cn(
11-
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11+
"flex h-10 w-full rounded-md border bg-slate-800/50 border-white/10 text-white placeholder:text-slate-400 ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500/40 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm px-3 py-2",
1212
className
1313
)}
1414
ref={ref}

src/components/ui/textarea.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
1010
return (
1111
<textarea
1212
className={cn(
13-
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
13+
"flex min-h-[80px] w-full rounded-md border bg-slate-800/50 border-white/10 text-white placeholder:text-slate-400 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500/40 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 px-3 py-2",
1414
className
1515
)}
1616
ref={ref}

src/config/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export const API_CONFIG = {
99

1010
ENDPOINTS: {
1111
CHAT: '/api/chat',
12-
HEALTH: '/api/health'
12+
HEALTH: '/api/health',
13+
CONTACT: '/api/contact'
1314
}
1415
};
1516

src/data/personalInfo.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,43 @@ export const personalInfo = {
2424
],
2525
experience: [
2626
{
27-
title: "AI Engineer Intern (Incoming)",
27+
title: "AI Engineer Intern",
2828
company: "DIET, Harvard University",
29-
duration: "Sep 2025 – Dec 2025",
30-
description: "Developing an end-to-end AI pipeline to assist in the identification of stolen ancient coins using image segmentation, embedding-based retrieval, and auction data integration."
29+
duration: "Aug 2025 – Present",
30+
description: "Architected an agentic system and fine-tuned multimodal VLMs for artwork analysis at Harvard Arts Museum.",
3131
},
3232
{
3333
title: "Machine Learning Engineer Intern",
3434
company: "Institute of Artificial Intelligence, Nanjing Normal University",
35-
duration: "Aug 2023 - Jun 2024",
36-
description: "Independently developed a diffraction-pattern recognition algorithm using computer vision and deep learning to automate RHEED image analysis, leading the end-to-end algorithm design and implementation."
35+
duration: "Aug 2023 Jun 2024",
36+
description: "Designed a production-grade computer vision system for RHEED image analysis; led a 6-member team and achieved substantial time and cost savings.",
3737
}
3838
],
3939
projects: [
4040
{
41-
name: "AI and Education",
42-
description: "Led the team to design modular AI solutions for educational applications, focusing on model architecture and content generation pipelines. Published paper 'A Comprehensive Investigation for ChatGPT's Applications in Education'.",
43-
technologies: ["Model Architecture", "Product Design", "Content Generation"]
41+
name: "Interactive Artwork Image Analysis Agent",
42+
description: "Agentic system that orchestrates 20+ V&L models/APIs (Qwen-VL, BLIP-2, GPT-4o, Gemini, YOLOv8) to analyze user-uploaded artwork images, producing structured metadata; reduced cost by 40% and improved speed by 58%.",
43+
technologies: ["Agentic System", "Qwen-VL", "BLIP-2", "GPT-4o", "Gemini", "YOLOv8"]
44+
},
45+
{
46+
name: "Multimodal VLM Fine-Tuning for Artwork Description",
47+
description: "Fine-tuned Qwen2.5-VL-7B with LoRA on curated artwork datasets; leveraged ViT and Qwen-VL on 385K+ images to generate 70M+ tags/descriptions using warm-up, cosine LR decay, BF16, and gradient accumulation.",
48+
technologies: ["Qwen2.5-VL-7B", "LoRA", "SFT", "BF16", "Cosine LR Decay", "Warm-up", "Gradient Accumulation", "ViT"]
49+
},
50+
{
51+
name: "AI Chat Website with RAG System",
52+
description: "Developed a full-stack AI chat application using React, TypeScript, and OpenAI GPT API. Implemented RAG (Retrieval-Augmented Generation) with ChromaDB vector database, MMR algorithm for diverse retrieval, and deployed on Railway backend with GitHub Pages frontend.",
53+
technologies: ["React", "TypeScript", "OpenAI API", "RAG", "ChromaDB", "Vector Search", "Full-Stack"]
4454
},
4555
{
4656
name: "Boston Police Department Budget Analysis",
4757
description: "Performed data cleaning, exploratory data analysis and visualization on BPD budget and payroll records to uncover trends in overtime spending. Built and evaluated machine learning models to predict future overtime expenditures.",
4858
technologies: ["Data Analysis", "Machine Learning", "Data Visualization", "GitHub"]
4959
},
5060
{
51-
name: "Machine-Vision Based Assistance System",
52-
description: "Built an accessibility assistance system using YOLO-based object detection and semantic segmentation for blind pathways. Responsible for technical architecture, dataset collection, and model development.",
53-
technologies: ["YOLO", "Object Detection", "Semantic Segmentation"]
61+
name: "Machine-Vision Based Assistance System",
62+
description: "Built an accessibility assistance system using YOLO-based object detection and semantic segmentation for blind pathways. Responsible for technical architecture, dataset collection, and model development.",
63+
technologies: ["YOLO", "Object Detection", "Semantic Segmentation"]
5464
},
5565
{
5666
name: "Patent: Compound Flood Disaster Prediction",

src/pages/Index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Link, useLocation } from "react-router-dom";
22
import ProfileSection from "@/components/ProfileSection";
33
import ChatSection from "@/components/ChatSection";
4+
import GetInTouchSection from "@/components/GetInTouchSection";
45

56
const Index = () => {
67
const location = useLocation();
@@ -58,6 +59,9 @@ const Index = () => {
5859

5960
{/* Chat Section */}
6061
<ChatSection />
62+
63+
{/* Get In Touch Section */}
64+
<GetInTouchSection />
6165
</div>
6266
</div>
6367
);

0 commit comments

Comments
 (0)